From d7521c53ae9511a2a3c18f43bb1e61559c03e0e2 Mon Sep 17 00:00:00 2001 From: Miguel Jimenez Esun Date: Wed, 19 Jul 2017 10:57:56 -0700 Subject: [PATCH] BREAKING: Add regenerator-runtime on demand, based on the files Summary: Adding a Babel plugin that will analyze the file looking for any potential candidate to use `regenerator-runtime`, and if so, will inject dynamically the module. The module is injected per file, so we avoid polluting the global environment. The plugin is also able to inject the `require` call beforehand, so that the inliner can pick them and inline them. The Babel plugin is part of `react-native-babel-preset`, so as long as you are using this preset you are safe. If not, you should include the specific transformer into your list of plugins, as `react-native-babel-preset/transforms/transform-regenerator-runtime-insertion.js`. Reviewed By: davidaurelio Differential Revision: D5388655 fbshipit-source-id: dc403f3d5e2d807529eb8569a85c45fec36a6a3e --- lerna.json | 2 +- packages/metro-bundler/package.json | 4 +- .../worker/__tests__/constant-folding-test.js | 134 +++++++++++++----- .../worker/__tests__/worker-test.js | 2 +- .../JSTransformer/worker/constant-folding.js | 34 +++-- .../src/JSTransformer/worker/index.js | 31 ++-- .../src/JSTransformer/worker/inline.js | 34 ++--- .../src/JSTransformer/worker/types.flow.js | 21 +++ packages/metro-bundler/yarn.lock | 6 +- 9 files changed, 182 insertions(+), 86 deletions(-) create mode 100644 packages/metro-bundler/src/JSTransformer/worker/types.flow.js diff --git a/lerna.json b/lerna.json index 8f5b93ff..5af73b6a 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "0.9.0", + "version": "0.10.0", "lerna": "2.0.0-rc.5", "npmClient": "yarn", "packages": [ diff --git a/packages/metro-bundler/package.json b/packages/metro-bundler/package.json index 5b0f1761..9b49463d 100644 --- a/packages/metro-bundler/package.json +++ b/packages/metro-bundler/package.json @@ -1,5 +1,5 @@ { - "version": "0.9.0", + "version": "0.10.0", "name": "metro-bundler", "description": "🚇 The JavaScript bundler for React Native.", "main": "src/index.js", @@ -19,7 +19,7 @@ "babel-plugin-external-helpers": "^6.18.0", "babel-preset-es2015-node": "^6.1.1", "babel-preset-fbjs": "^2.1.4", - "babel-preset-react-native": "^2.0.0", + "babel-preset-react-native": "^2.1.0", "babel-register": "^6.24.1", "babylon": "^6.17.0", "chalk": "^1.1.1", diff --git a/packages/metro-bundler/src/JSTransformer/worker/__tests__/constant-folding-test.js b/packages/metro-bundler/src/JSTransformer/worker/__tests__/constant-folding-test.js index bd15a1c6..e289b379 100644 --- a/packages/metro-bundler/src/JSTransformer/worker/__tests__/constant-folding-test.js +++ b/packages/metro-bundler/src/JSTransformer/worker/__tests__/constant-folding-test.js @@ -10,26 +10,28 @@ */ 'use strict'; -const babel = require('babel-core'); +/* eslint-disable max-len */ + const constantFolding = require('../constant-folding'); -function parse(code) { - return babel.transform(code, {code: false, babelrc: false, compact: true}); -} +const {transform, transformFromAst} = require('babel-core'); const babelOptions = { babelrc: false, compact: true, - retainLines: false, }; -function normalize({code}) { - return babel.transform(code, babelOptions).code; +function toString(ast) { + return normalize(transformFromAst(ast, babelOptions).code); +} + +function normalize(code) { + return transform(code, babelOptions).code; } describe('constant expressions', () => { it('can optimize conditional expressions with constant conditions', () => { - const code = ` + const before = ` a( 'production'=="production", 'production'!=='development', @@ -39,56 +41,97 @@ describe('constant expressions', () => { 'android'==='android' ? {a:1} : {a:0}, 'foo'==='bar' ? b : c, f() ? g() : h() - );`; - expect(normalize(constantFolding('arbitrary.js', parse(code)))).toEqual( - 'a(true,true,2,true,{},{a:1},c,f()?g():h());', - ); + ); + `; + + const after = ` + a( + true, + true, + 2, + true, + {}, + {a:1}, + c, + f() ? g() : h() + ); + `; + + const {ast} = constantFolding('arbitrary.js', {code: before}); + expect(toString(ast)).toEqual(normalize(after)); }); it('can optimize ternary expressions with constant conditions', () => { - const code = `var a = true ? 1 : 2; - var b = 'android' == 'android' - ? ('production' != 'production' ? 'a' : 'A') - : 'i';`; - expect(normalize(constantFolding('arbitrary.js', parse(code)))).toEqual( - "var a=1;var b='A';", - ); + const before = ` + var a = true ? 1 : 2; + var b = 'android' == 'android' + ? ('production' != 'production' ? 'a' : 'A') + : 'i'; + `; + + const after = ` + var a = 1; + var b = 'A'; + `; + + const {ast} = constantFolding('arbitrary.js', {code: before}); + expect(toString(ast)).toEqual(normalize(after)); }); it('can optimize logical operator expressions with constant conditions', () => { - const code = ` + const before = ` var a = true || 1; var b = 'android' == 'android' && - 'production' != 'production' || null || "A";`; - expect(normalize(constantFolding('arbitrary.js', parse(code)))).toEqual( - 'var a=true;var b="A";', - ); + 'production' != 'production' || null || "A"; + `; + + const after = ` + var a = true; + var b = "A"; + `; + + const {ast} = constantFolding('arbitrary.js', {code: before}); + expect(toString(ast)).toEqual(normalize(after)); }); it('can optimize logical operators with partly constant operands', () => { - const code = ` + const before = ` var a = "truthy" || z(); var b = "truthy" && z(); var c = null && z(); var d = null || z(); var e = !1 && z(); `; - expect(normalize(constantFolding('arbitrary.js', parse(code)))).toEqual( - 'var a="truthy";var b=z();var c=null;var d=z();var e=false;', - ); + + const after = ` + var a = "truthy"; + var b = z(); + var c = null; + var d = z(); + var e = false; + `; + + const {ast} = constantFolding('arbitrary.js', {code: before}); + expect(toString(ast)).toEqual(normalize(after)); }); it('can remode an if statement with a falsy constant test', () => { - const code = ` + const before = ` if ('production' === 'development' || false) { var a = 1; } `; - expect(normalize(constantFolding('arbitrary.js', parse(code)))).toEqual(''); + + // Intentionally empty: all dead code. + const after = ` + `; + + const {ast} = constantFolding('arbitrary.js', {code: before}); + expect(toString(ast)).toEqual(normalize(after)); }); it('can optimize if-else-branches with constant conditions', () => { - const code = ` + const before = ` if ('production' == 'development') { var a = 1; var b = a + 2; @@ -99,13 +142,20 @@ describe('constant expressions', () => { var a = 'b'; } `; - expect(normalize(constantFolding('arbitrary.js', parse(code)))).toEqual( - '{var a=3;var b=a+4;}', - ); + + const after = ` + { + var a = 3; + var b = a + 4; + } + `; + + const {ast} = constantFolding('arbitrary.js', {code: before}); + expect(toString(ast)).toEqual(normalize(after)); }); it('can optimize nested if-else constructs', () => { - const code = ` + const before = ` if ('ios' === "android") { if (true) { require('a'); @@ -120,8 +170,16 @@ describe('constant expressions', () => { } } `; - expect(normalize(constantFolding('arbitrary.js', parse(code)))).toEqual( - "{{require('c');}}", - ); + + const after = ` + { + { + require('c'); + } + } + `; + + const {ast} = constantFolding('arbitrary.js', {code: before}); + expect(toString(ast)).toEqual(normalize(after)); }); }); diff --git a/packages/metro-bundler/src/JSTransformer/worker/__tests__/worker-test.js b/packages/metro-bundler/src/JSTransformer/worker/__tests__/worker-test.js index da0aa494..aa8841f2 100644 --- a/packages/metro-bundler/src/JSTransformer/worker/__tests__/worker-test.js +++ b/packages/metro-bundler/src/JSTransformer/worker/__tests__/worker-test.js @@ -275,7 +275,7 @@ describe('code transformation worker:', () => { const inlineResult = {map: {version: 3, sources: []}, ast: {}}; inline.mockReturnValue(inlineResult); transformCode(transformer, filename, filename, 'code', options, () => { - expect(constantFolding).toBeCalledWith(filename, inlineResult); + expect(constantFolding).toBeCalledWith(filename, inlineResult, options); done(); }); }); diff --git a/packages/metro-bundler/src/JSTransformer/worker/constant-folding.js b/packages/metro-bundler/src/JSTransformer/worker/constant-folding.js index 46597935..d1b473c0 100644 --- a/packages/metro-bundler/src/JSTransformer/worker/constant-folding.js +++ b/packages/metro-bundler/src/JSTransformer/worker/constant-folding.js @@ -13,8 +13,9 @@ 'use strict'; const babel = require('babel-core'); +const invariant = require('fbjs/lib/invariant'); -import type {Ast, SourceMap as MappingsMap} from 'babel-core'; +import type {IntermediateTransformResult} from './types.flow'; const t = babel.types; const Conditional = { @@ -31,7 +32,7 @@ const Conditional = { }, }; -const plugin = { +const constantFoldingPlugin = { visitor: { BinaryExpression: { exit(path) { @@ -71,25 +72,32 @@ const plugin = { }, }; +const plugin = () => constantFoldingPlugin; + function constantFolding( filename: string, - transformResult: { - ast: Ast, - code?: ?string, - map: ?MappingsMap, - }, -) { - return babel.transformFromAst(transformResult.ast, transformResult.code, { + transformResult: IntermediateTransformResult, + options: {+dev: boolean, +platform: ?string}, +): IntermediateTransformResult { + const code = transformResult.code; + const babelOptions = { filename, - plugins: [plugin], + plugins: [[plugin, options]], inputSourceMap: transformResult.map, sourceMaps: true, sourceFileName: filename, + code: true, babelrc: false, compact: true, - retainLines: true, - }); + }; + + const result = transformResult.ast + ? babel.transformFromAst(transformResult.ast, code, babelOptions) + : (code && babel.transform(code, babelOptions)) || {}; + const {ast} = result; + invariant(ast != null, 'Missing AST in babel transform results.'); + return {ast, code: result.code, map: result.map}; } -constantFolding.plugin = plugin; +constantFolding.plugin = constantFoldingPlugin; module.exports = constantFolding; diff --git a/packages/metro-bundler/src/JSTransformer/worker/index.js b/packages/metro-bundler/src/JSTransformer/worker/index.js index 914e7b0d..478fa952 100644 --- a/packages/metro-bundler/src/JSTransformer/worker/index.js +++ b/packages/metro-bundler/src/JSTransformer/worker/index.js @@ -22,7 +22,8 @@ const minify = require('./minify'); import type {LogEntry} from '../../Logger/Types'; import type {MappingsMap} from '../../lib/SourceMap'; import type {LocalPath} from '../../node-haste/lib/toLocalPath'; -import type {Ast, Plugins as BabelPlugins} from 'babel-core'; +import type {IntermediateTransformResult} from './types.flow'; +import type {Plugins as BabelPlugins} from 'babel-core'; export type TransformedCode = { code: string, @@ -38,7 +39,7 @@ export type Transformer = { options: ExtraOptions & TransformOptions, plugins?: BabelPlugins, src: string, - |}) => {ast: ?Ast, code: string, map: ?MappingsMap}, + |}) => IntermediateTransformResult, getCacheKey: () => string, }; @@ -83,6 +84,8 @@ type TransformCode = ( Callback, ) => void; +const transformers = [inline, constantFolding]; + const transformCode: TransformCode = asyncify( ( transformer: Transformer<*>, @@ -121,17 +124,29 @@ const transformCode: TransformCode = asyncify( 'Missing transform results despite having no error.', ); - var code, map; + let code; + let map; + if (options.minify) { - ({code, map} = constantFolding( - filename, - inline(filename, transformed, options), - )); - invariant(code != null, 'Missing code from constant-folding transform.'); + let result = transformed; + const length = transformers.length; + + for (let i = 0; i < length; i++) { + result = transformers[i](filename, result, options); + } + + ({code, map} = result); } else { ({code, map} = transformed); } + invariant( + code != null, + 'The last transformer on the list (' + + transformers[transformers.length - 1].name + + ') has to output code', + ); + if (isJson) { code = code.replace(/^\w+\.exports=/, ''); } else { diff --git a/packages/metro-bundler/src/JSTransformer/worker/inline.js b/packages/metro-bundler/src/JSTransformer/worker/inline.js index 57ce5f01..6ea73726 100644 --- a/packages/metro-bundler/src/JSTransformer/worker/inline.js +++ b/packages/metro-bundler/src/JSTransformer/worker/inline.js @@ -15,7 +15,7 @@ const babel = require('babel-core'); const invariant = require('fbjs/lib/invariant'); -import type {Ast, SourceMap as MappingsMap} from 'babel-core'; +import type {IntermediateTransformResult} from './types.flow'; const t = babel.types; const React = {name: 'React'}; @@ -115,6 +115,16 @@ function findProperty(objectExpression, key, fallback) { return property ? property.value : fallback(); } +function checkRequireArgs(args, dependencyId) { + const pattern = t.stringLiteral(dependencyId); + return ( + t.isStringLiteral(args[0], pattern) || + (t.isMemberExpression(args[0]) && + t.isNumericLiteral(args[0].property) && + t.isStringLiteral(args[1], pattern)) + ); +} + const inlinePlugin = { visitor: { Identifier(path, state) { @@ -162,27 +172,11 @@ const inlinePlugin = { const plugin = () => inlinePlugin; -function checkRequireArgs(args, dependencyId) { - const pattern = t.stringLiteral(dependencyId); - return ( - t.isStringLiteral(args[0], pattern) || - (t.isMemberExpression(args[0]) && - t.isNumericLiteral(args[0].property) && - t.isStringLiteral(args[1], pattern)) - ); -} - -type AstResult = { - ast: Ast, - code: ?string, - map: ?MappingsMap, -}; - function inline( filename: string, - transformResult: {ast?: ?Ast, code: string, map: ?MappingsMap}, + transformResult: IntermediateTransformResult, options: {+dev: boolean, +platform: ?string}, -): AstResult { +): IntermediateTransformResult { const code = transformResult.code; const babelOptions = { filename, @@ -197,7 +191,7 @@ function inline( const result = transformResult.ast ? babel.transformFromAst(transformResult.ast, code, babelOptions) - : babel.transform(code, babelOptions); + : (code && babel.transform(code, babelOptions)) || {}; const {ast} = result; invariant(ast != null, 'Missing AST in babel transform results.'); return {ast, code: result.code, map: result.map}; diff --git a/packages/metro-bundler/src/JSTransformer/worker/types.flow.js b/packages/metro-bundler/src/JSTransformer/worker/types.flow.js new file mode 100644 index 00000000..2fe73d85 --- /dev/null +++ b/packages/metro-bundler/src/JSTransformer/worker/types.flow.js @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2017-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. + * + * @flow + * @format + */ + +'use strict'; + +import type {Ast, SourceMap as MappingsMap} from 'babel-core'; + +export type IntermediateTransformResult = { + ast: ?Ast, + code: ?string, + map: ?MappingsMap, +}; diff --git a/packages/metro-bundler/yarn.lock b/packages/metro-bundler/yarn.lock index 6f112469..4be80f7f 100644 --- a/packages/metro-bundler/yarn.lock +++ b/packages/metro-bundler/yarn.lock @@ -536,9 +536,9 @@ babel-preset-fbjs@^2.1.4: babel-plugin-transform-react-display-name "^6.8.0" babel-plugin-transform-react-jsx "^6.8.0" -babel-preset-react-native@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/babel-preset-react-native/-/babel-preset-react-native-2.0.0.tgz#c26c7066c7399df30926fa03c012ef87f2cce5b7" +babel-preset-react-native@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/babel-preset-react-native/-/babel-preset-react-native-2.1.0.tgz#9013ebd82da1c88102bf588810ff59e209ca2b8a" dependencies: babel-plugin-check-es2015-constants "^6.5.0" babel-plugin-react-transform "2.0.2"