diff --git a/packages/babel-plugin-inline-view-configs/__test_fixtures__/failures.js b/packages/babel-plugin-inline-view-configs/__test_fixtures__/failures.js new file mode 100644 index 00000000000..7baa1bb435f --- /dev/null +++ b/packages/babel-plugin-inline-view-configs/__test_fixtures__/failures.js @@ -0,0 +1,79 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +'use strict'; + +const COMMANDS_EXPORTED_WITH_DIFFERENT_NAME = ` +// @flow + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {ViewProps} from 'ViewPropTypes'; + +type ModuleProps = $ReadOnly<{| + ...ViewProps, +|}>; + +interface NativeCommands { + +hotspotUpdate: (viewRef: React.Ref<'Module'>) => void; +} + +export const Foo = codegenNativeCommands(); + +export default codegenNativeComponent('Module'); +`; + +const OTHER_COMMANDS_EXPORT = ` +// @flow + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {ViewProps} from 'ViewPropTypes'; + +type ModuleProps = $ReadOnly<{| + ...ViewProps, +|}>; + +interface NativeCommands { + +hotspotUpdate: (viewRef: React.Ref<'Module'>) => void; +} + +export const Commands = 4; + +export default codegenNativeComponent('Module'); +`; + +const COMMANDS_EXPORTED_WITH_SHORTHAND = ` +// @flow + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {ViewProps} from 'ViewPropTypes'; + +type ModuleProps = $ReadOnly<{| + ...ViewProps, +|}>; + +interface NativeCommands { + +hotspotUpdate: (viewRef: React.Ref<'Module'>) => void; +} + +const Commands = 4; + +export {Commands}; + +export default codegenNativeComponent('Module'); +`; + +module.exports = { + 'CommandsExportedWithDifferentNameNativeComponent.js': COMMANDS_EXPORTED_WITH_DIFFERENT_NAME, + 'CommandsExportedWithShorthandNativeComponent.js': COMMANDS_EXPORTED_WITH_SHORTHAND, + 'OtherCommandsExportNativeComponent.js': OTHER_COMMANDS_EXPORT, +}; diff --git a/packages/babel-plugin-inline-view-configs/__tests__/__snapshots__/index-test.js.snap b/packages/babel-plugin-inline-view-configs/__tests__/__snapshots__/index-test.js.snap index 9d203135ccd..26e12919da2 100644 --- a/packages/babel-plugin-inline-view-configs/__tests__/__snapshots__/index-test.js.snap +++ b/packages/babel-plugin-inline-view-configs/__tests__/__snapshots__/index-test.js.snap @@ -19,7 +19,6 @@ type ModuleProps = $ReadOnly<{| ...ViewProps, onDirectEventDefinedInlineNull: DirectEventHandler, onBubblingEventDefinedInlineNull: BubblingEventHandler, |}>; -export const Commands = codegenNativeCommands(); const registerGeneratedViewConfig = require('registerGeneratedViewConfig'); @@ -73,3 +72,9 @@ exports[`Babel plugin inline view configs can inline config for NotANativeCompon export default 'Not a view config';" `; + +exports[`Babel plugin inline view configs fails on inline config for CommandsExportedWithDifferentNameNativeComponent.js 1`] = `"Native commands must be exported with the name 'Commands'"`; + +exports[`Babel plugin inline view configs fails on inline config for CommandsExportedWithShorthandNativeComponent.js 1`] = `"'Commands' is a reserved export and may only be used to export the result of codegenNativeCommands."`; + +exports[`Babel plugin inline view configs fails on inline config for OtherCommandsExportNativeComponent.js 1`] = `"'Commands' is a reserved export and may only be used to export the result of codegenNativeCommands."`; diff --git a/packages/babel-plugin-inline-view-configs/__tests__/index-test.js b/packages/babel-plugin-inline-view-configs/__tests__/index-test.js index da09f29a5df..26c0e09ad06 100644 --- a/packages/babel-plugin-inline-view-configs/__tests__/index-test.js +++ b/packages/babel-plugin-inline-view-configs/__tests__/index-test.js @@ -12,9 +12,10 @@ const {transform: babelTransform} = require('@babel/core'); const fixtures = require('../__test_fixtures__/fixtures.js'); +const failures = require('../__test_fixtures__/failures.js'); -function transform(filename) { - return babelTransform(fixtures[filename], { +function transform(fixture, filename) { + return babelTransform(fixture, { plugins: [require('@babel/plugin-syntax-flow'), require('../index')], babelrc: false, filename, @@ -26,7 +27,17 @@ describe('Babel plugin inline view configs', () => { .sort() .forEach(fixtureName => { it(`can inline config for ${fixtureName}`, () => { - expect(transform(fixtureName)).toMatchSnapshot(); + expect(transform(fixtures[fixtureName], fixtureName)).toMatchSnapshot(); + }); + }); + + Object.keys(failures) + .sort() + .forEach(fixtureName => { + it(`fails on inline config for ${fixtureName}`, () => { + expect(() => { + transform(failures[fixtureName], fixtureName); + }).toThrowErrorMatchingSnapshot(); }); }); }); diff --git a/packages/babel-plugin-inline-view-configs/index.js b/packages/babel-plugin-inline-view-configs/index.js index 992406c7eb3..a6dc270b284 100644 --- a/packages/babel-plugin-inline-view-configs/index.js +++ b/packages/babel-plugin-inline-view-configs/index.js @@ -51,14 +51,83 @@ module.exports = function(context) { pre(state) { this.code = state.code; this.filename = state.opts.filename; + this.defaultExport = null; + this.commandsExport = null; + this.codeInserted = false; }, visitor: { + ExportNamedDeclaration(nodePath) { + if (this.codeInserted) { + return; + } + + if ( + nodePath.node.declaration && + nodePath.node.declaration.declarations && + nodePath.node.declaration.declarations[0] + ) { + const firstDeclaration = nodePath.node.declaration.declarations[0]; + + if (firstDeclaration.type === 'VariableDeclarator') { + if ( + firstDeclaration.init.type === 'CallExpression' && + firstDeclaration.init.callee.type === 'Identifier' && + firstDeclaration.init.callee.name === 'codegenNativeCommands' + ) { + if ( + firstDeclaration.id.type === 'Identifier' && + firstDeclaration.id.name !== 'Commands' + ) { + throw new Error( + "Native commands must be exported with the name 'Commands'", + ); + } + this.commandsExport = nodePath; + return; + } else { + if (firstDeclaration.id.name === 'Commands') { + throw new Error( + "'Commands' is a reserved export and may only be used to export the result of codegenNativeCommands.", + ); + } + } + } + } else if ( + nodePath.node.specifiers && + nodePath.node.specifiers.length > 0 + ) { + nodePath.node.specifiers.forEach(specifier => { + if ( + specifier.type === 'ExportSpecifier' && + specifier.local.type === 'Identifier' && + specifier.local.name === 'Commands' + ) { + throw new Error( + "'Commands' is a reserved export and may only be used to export the result of codegenNativeCommands.", + ); + } + }); + } + }, ExportDefaultDeclaration(nodePath, state) { if (isCodegenDeclaration(nodePath.node.declaration)) { - const viewConfig = generateViewConfig(this.filename, this.code); - nodePath.replaceWithMultiple(context.parse(viewConfig).program.body); + this.defaultExport = nodePath; } }, + Program: { + exit() { + if (this.defaultExport) { + const viewConfig = generateViewConfig(this.filename, this.code); + this.defaultExport.replaceWithMultiple( + context.parse(viewConfig).program.body, + ); + if (this.commandsExport != null) { + this.commandsExport.remove(); + } + this.codeInserted = true; + } + }, + }, }, }; }; diff --git a/packages/babel-plugin-inline-view-configs/package.json b/packages/babel-plugin-inline-view-configs/package.json index 3c4c65c51b0..e35d93281e0 100644 --- a/packages/babel-plugin-inline-view-configs/package.json +++ b/packages/babel-plugin-inline-view-configs/package.json @@ -1,5 +1,5 @@ { - "version": "0.0.1", + "version": "0.0.2", "name": "babel-plugin-inline-view-configs", "description": "Babel plugin to inline view configs for React Native", "repository": {