diff --git a/.gitignore b/.gitignore index 3c99356..0751f9c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ /node_modules/ /html-report/ /lib-cov/ +/package-lock.json diff --git a/src/index.js b/src/index.js index 4ef9011..4f92131 100644 --- a/src/index.js +++ b/src/index.js @@ -1,26 +1,45 @@ export default function ({ types: t }) { return { visitor: { - JSXOpeningElement(path, { file }) { - file.set('hasJSX', true); - }, - Program: { enter(path, { file }) { - file.set('hasJSX', false); + const ourNode = t.importDeclaration([ + t.importDefaultSpecifier(t.identifier('React')), + ], t.stringLiteral('react')); + + // Add an import early, so that other plugins get to see it + file.set('ourPath', path.unshiftContainer('body', ourNode)[0]); }, - exit({ node, scope }, { file }) { - if (!(file.get('hasJSX') && !scope.hasBinding('React'))) { - return; + exit(_, { file }) { + // If our import is still intact and we haven't encountered any JSX in + // the program, then we just remove it. There's an edge case, where + // some other plugin could add JSX in its `Program.exit`, so our + // `JSXOpeningElement` will trigger only after this method, but it's + // likely that said plugin will also add a React import too. + const ourPath = file.get('ourPath'); + if (ourPath && !file.get('hasJSX')) { + if (!ourPath.removed) ourPath.remove(); + file.set('ourPath', undefined); } + }, + }, - const reactImportDeclaration = t.importDeclaration([ - t.importDefaultSpecifier(t.identifier('React')), - ], t.stringLiteral('react')); + ImportDeclaration(path, { file }) { + // Return early if this has nothing to do with React + if (path.node.specifiers.every(x => x.local.name !== 'React')) return; - node.body.unshift(reactImportDeclaration); - }, + // If our import is still intact and we encounter some other import + // which also imports `React`, then we remove ours. + const ourPath = file.get('ourPath'); + if (ourPath && path !== ourPath) { + if (!ourPath.removed) ourPath.remove(); + file.set('ourPath', undefined); + } + }, + + JSXOpeningElement(_, { file }) { + file.set('hasJSX', true); }, }, }; diff --git a/test/indexTest.js b/test/indexTest.js index 63404b2..bb8b4e4 100644 --- a/test/indexTest.js +++ b/test/indexTest.js @@ -10,17 +10,50 @@ try { reactPlugin = require('../src/index').default; } +const somePluginEnter = ({ types: t }) => ({ + visitor: { + Program(path) { + path.unshiftContainer('body', t.importDeclaration([ + t.importDefaultSpecifier(t.identifier('React')), + ], t.stringLiteral('react'))); + }, + }, +}); + +const somePluginExit = ({ types: t }) => ({ + visitor: { + Program: { + exit(path) { + path.unshiftContainer('body', t.importDeclaration([ + t.importDefaultSpecifier(t.identifier('React')), + ], t.stringLiteral('react'))); + }, + }, + }, +}); + +const somePluginCrazy = () => ({ + visitor: { + Program(_, { file }) { + file.get('ourPath').remove(); + }, + }, +}); + +const genericInput = 'export default class Component {render() {return
}}'; +const genericOutput = 'import React from "react";\nexport default class Component {\n render() {\n return
;\n }\n}'; + describe('babel-plugin-react', () => { beforeEach(() => { - transform = code => babel.transform(code, { - plugins: ['syntax-jsx', reactPlugin], + transform = (code, pluginsBefore = [], pluginsAfter = []) => babel.transform(code, { + plugins: ['syntax-jsx', ...pluginsBefore, reactPlugin, ...pluginsAfter], }).code; }); it('should return transpiled code with required React', () => { - const transformed = transform('export default class Component {render() {return
}}'); + const transformed = transform(genericInput); - assert.equal(transformed, 'import React from "react";\nexport default class Component {\n render() {\n return
;\n }\n}'); + assert.equal(transformed, genericOutput); }); it('should return not transpiled code', () => { @@ -41,4 +74,15 @@ describe('babel-plugin-react', () => { assert.equal(transformed, 'import React from "react/addons";\nclass Component {\n render() {\n return
;\n }\n}'); }); + + it('should get along with other plugins which add React import', () => { + assert.equal(transform(genericInput, [somePluginEnter]), genericOutput); + assert.equal(transform(genericInput, [somePluginExit]), genericOutput); + assert.equal(transform(genericInput, [], [somePluginEnter]), genericOutput); + assert.equal(transform(genericInput, [], [somePluginExit]), genericOutput); + }); + + it('should not blow up if another plugin removes our import', () => { + assert.equal(transform(genericInput, [], [somePluginCrazy]), 'export default class Component {\n render() {\n return
;\n }\n}'); + }); });