From 95283a005d6f3f2b0a296f37b47b37efdd1051b8 Mon Sep 17 00:00:00 2001 From: Christopher Garrett Date: Wed, 7 Nov 2018 21:12:19 -0800 Subject: [PATCH] [FEAT] Adds an option to emit external babel helpers This PR adds the `includeExternalHelpers` config option which allows users to explicitly opt into using externalized helpers for transpiled code. In large apps this can help with app size, especially in apps which are making heavy usage of new ES features like native classes and decorators. This option is a _global_ option, meaning it _must_ be configured in the root application. The reason for this is because there must be one canonical source of truth for which version of helpers are being used, and whether or not helpers are being used at all. If one addon decides to use helpers, the app must include them, and they must be the right version. The current setup will _only_ support babel 7 helpers. This can be expanded upon in the future, but the complexity of doing so makes it beyond the scope of the initial work here. --- .eslintrc.js | 1 + README.md | 24 +++++++++++ ember-cli-build.js | 3 +- index.js | 66 +++++++++++++++++++++++++++-- lib/inject-babel-helpers.js | 23 ++++++++++ lib/relative-module-paths.js | 1 - node-tests/addon-test.js | 31 +++++++++++++- package.json | 3 ++ tests/unit/external-helpers-test.js | 14 ++++++ yarn.lock | 23 ++++++++++ 10 files changed, 183 insertions(+), 6 deletions(-) create mode 100644 lib/inject-babel-helpers.js create mode 100644 tests/unit/external-helpers-test.js diff --git a/.eslintrc.js b/.eslintrc.js index 5060f182..b54d50f7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -26,6 +26,7 @@ module.exports = { 'blueprints/*/index.js', 'config/**/*.js', 'tests/dummy/config/**/*.js', + 'lib/**/*.js' ], excludedFiles: [ 'addon/**', diff --git a/README.md b/README.md index f5045381..828c6133 100755 --- a/README.md +++ b/README.md @@ -94,6 +94,7 @@ interface EmberCLIBabelConfig { */ 'ember-cli-babel'?: { includePolyfill?: boolean; + includeExternalHelpers?: boolean; compileModules?: boolean; disableDebugTooling?: boolean; disablePresetEnv?: boolean; @@ -159,6 +160,29 @@ let app = new EmberApp(defaults, { }); ``` +#### External Helpers + +Babel often includes helper functions to handle some of the more complex logic +in codemods. These functions are inlined by default, so they are duplicated in +every file that they are used in, which adds some extra weight to final builds. + +Enabling `includeExternalHelpers` will cause Babel to import these helpers from +a shared module, reducing app size overall. This option is available _only_ to +the root application, because it is a global configuration value. There must be +one agreed upon Babel version to generate helpers, and if any addon opts into +generating helpers then they must be included and synced with any other addons +and the root app. + +```js +// ember-cli-build.js + +let app = new EmberApp(defaults, { + 'ember-cli-babel': { + includeExternalHelpers: true + } +}); +``` + #### Enabling Source Maps Babel generated source maps will enable you to debug your original ES6 source diff --git a/ember-cli-build.js b/ember-cli-build.js index fd55bf87..285dcf8e 100644 --- a/ember-cli-build.js +++ b/ember-cli-build.js @@ -19,10 +19,11 @@ module.exports = function(defaults) { let app = new EmberAddon(defaults, { 'ember-cli-babel': { includePolyfill: true, + includeExternalHelpers: true, // ember-cli-babels defaults, should be parallelizable. If they are not, // it should fail to build. This flag being enabled ensure that to be the // case. - throwUnlessParallelizable: true + throwUnlessParallelizable: true, } }); diff --git a/index.js b/index.js index 32eacf69..6899f205 100644 --- a/index.js +++ b/index.js @@ -84,8 +84,58 @@ module.exports = { } }, + _shouldIncludeHelpers() { + let hostOptions = this._getHostOptions(); + let customOptions = hostOptions['ember-cli-babel']; + + if (customOptions && 'includeExternalHelpers' in customOptions) { + return customOptions.includeExternalHelpers === true; + } else { + return false; + } + }, + + _getHelperVersion() { + if (!this._helperVersion) { + let checker = new VersionChecker(this.project); + this._helperVersion = checker.for('@babel/core', 'npm').version; + } + + return this._helperVersion; + }, + + _getHelpersPlugin() { + return [ + [ + require.resolve('./lib/inject-babel-helpers'), + { + helperVersion: this._getHelperVersion(), + } + ] + ] + }, + + treeForAddon() { + // Helpers are a global config, so only the root application should bother + // generating and including the file. + if (!(this.parent === this.project && this._shouldIncludeHelpers())) return; + + const babel = require('@babel/core'); + const writeFile = require('broccoli-file-creator'); + + let helpers = babel.buildExternalHelpers(null, 'module'); + let babelHelpersTree = writeFile('ember-cli-babel/-private/helpers.js', helpers); + + return this.transpileTree(babelHelpersTree, { + 'ember-cli-babel': { + // prevents the helpers from being double transpiled, and including themselves + disablePresetEnv: true + } + }); + }, + treeForVendor() { - if (!this._shouldIncludePolyfill()) { return; } + if (!this._shouldIncludePolyfill()) return; const Funnel = require('broccoli-funnel'); const UnwatchedDir = require('broccoli-source').UnwatchedDir; @@ -93,10 +143,12 @@ module.exports = { // Find babel-core's browser polyfill and use its directory as our vendor tree let polyfillDir = path.dirname(require.resolve('@babel/polyfill/dist/polyfill')); - return new Funnel(new UnwatchedDir(polyfillDir), { + let polyfillTree = new Funnel(new UnwatchedDir(polyfillDir), { destDir: 'babel-polyfill' }); - }, + + return polyfillTree; +}, included: function(app) { this._super.included.apply(this, arguments); @@ -123,6 +175,12 @@ module.exports = { return (this.parent && this.parent.options) || (this.app && this.app.options) || {}; }, + _getHostOptions() { + let host = this._findHost(); + + return (host && host.options) || {}; + }, + _parentName() { let parentName; @@ -158,6 +216,7 @@ module.exports = { _getBabelOptions(config) { let addonProvidedConfig = this._getAddonProvidedConfig(config); let shouldCompileModules = this._shouldCompileModules(config); + let shouldIncludeHelpers = this._shouldIncludeHelpers(); let emberCLIBabelConfig = config['ember-cli-babel']; let shouldRunPresetEnv = true; @@ -188,6 +247,7 @@ module.exports = { let userPostTransformPlugins = addonProvidedConfig.postTransformPlugins; options.plugins = [].concat( + shouldIncludeHelpers && this._getHelpersPlugin(), userPlugins, this._getDebugMacroPlugins(config), this._getEmberModulesAPIPolyfill(config), diff --git a/lib/inject-babel-helpers.js b/lib/inject-babel-helpers.js new file mode 100644 index 00000000..ce0e554e --- /dev/null +++ b/lib/inject-babel-helpers.js @@ -0,0 +1,23 @@ +let { addNamed } = require('@babel/helper-module-imports') + +module.exports = function injectBabelHelpers(options) { + const { helperVersion = "7.0.0-beta.0" } = options; + + return { + pre(file) { + file.set('helperGenerator', name => { + // If the helper didn't exist yet at the version given, we bail + // out and let Babel either insert it directly, or throw an error + // so that plugins can handle that case properly. + if ( + file.availableHelper && + !file.availableHelper(name, helperVersion) + ) { + return; + } + + return addNamed(file.path, name, 'ember-cli-babel/-private/helpers'); + }); + }, + }; +} diff --git a/lib/relative-module-paths.js b/lib/relative-module-paths.js index b456b14e..4e1ac7ef 100644 --- a/lib/relative-module-paths.js +++ b/lib/relative-module-paths.js @@ -1,4 +1,3 @@ -/* eslint-env node */ 'use strict'; const path = require('path'); diff --git a/node-tests/addon-test.js b/node-tests/addon-test.js index 992d0ef8..d606fca1 100644 --- a/node-tests/addon-test.js +++ b/node-tests/addon-test.js @@ -13,7 +13,11 @@ const createBuilder = BroccoliTestHelper.createBuilder; const createTempDir = BroccoliTestHelper.createTempDir; const terminateWorkerPool = require('./utils/terminate-workers'); -let Addon = CoreObject.extend(AddonMixin); +let Addon = CoreObject.extend(AddonMixin).extend({ + _findHost() { + return this.__host; + } +}); describe('ember-cli-babel', function() { @@ -23,6 +27,7 @@ describe('ember-cli-babel', function() { this.ui = new MockUI(); let project = { root: __dirname, emberCLIVersion: () => '2.16.2' }; this.addon = new Addon({ + __host: { options: {} }, project, parent: project, ui: this.ui, @@ -456,6 +461,30 @@ describe('ember-cli-babel', function() { }); }); + describe('_shouldIncludeHelpers()', function() { + it('should return false without any includeExternalHelpers option set', function() { + expect(this.addon._shouldIncludeHelpers()).to.be.false; + }); + + it('should return false with ember-cli-babel.includeExternalHelpers = true in parent', function() { + this.addon.parent.options = { 'ember-cli-babel': { includeExternalHelpers: true } }; + + expect(this.addon._shouldIncludeHelpers()).to.be.false; + }); + + it('should return true with ember-cli-babel.includeExternalHelpers = true in host', function() { + this.addon.__host.options = { 'ember-cli-babel': { includeExternalHelpers: true } }; + + expect(this.addon._shouldIncludeHelpers()).to.be.true; + }); + + it('should return false with ember-cli-babel.includeExternalHelpers = false in host', function() { + this.addon.__host.options = { 'ember-cli-babel': { includeExternalHelpers: false } }; + + expect(this.addon._shouldIncludeHelpers()).to.be.false; + }); + }); + describe('_shouldCompileModules()', function() { beforeEach(function() { this.addon.parent = { diff --git a/package.json b/package.json index 55998d4c..6cebbae5 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ }, "dependencies": { "@babel/core": "^7.0.0", + "@babel/plugin-external-helpers": "^7.0.0", "@babel/plugin-transform-modules-amd": "^7.0.0", "@babel/polyfill": "^7.0.0", "@babel/preset-env": "^7.0.0", @@ -48,7 +49,9 @@ "babel-plugin-module-resolver": "^3.1.1", "broccoli-babel-transpiler": "^7.1.0", "broccoli-debug": "^0.6.4", + "broccoli-file-creator": "^2.1.1", "broccoli-funnel": "^2.0.1", + "broccoli-merge-trees": "^3.0.1", "broccoli-source": "^1.1.0", "clone": "^2.1.2", "ember-cli-version-checker": "^2.1.2", diff --git a/tests/unit/external-helpers-test.js b/tests/unit/external-helpers-test.js new file mode 100644 index 00000000..7ebeab7a --- /dev/null +++ b/tests/unit/external-helpers-test.js @@ -0,0 +1,14 @@ +import { module, test } from 'qunit'; + +module('Unit | external helpers test', function() { + test('external helpers work ', function(assert) { + assert.expect(0); + + // This test will transpile to use external helpers depending on targets. If + // those helpers are not present, it will break. If IE11 is removed from + // targets we should find another way to test this. + class Foo {} + + new Foo(); + }); +}); diff --git a/yarn.lock b/yarn.lock index 587d3751..174b7a9c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -193,6 +193,13 @@ version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.0.0.tgz#697655183394facffb063437ddf52c0277698775" +"@babel/plugin-external-helpers@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-external-helpers/-/plugin-external-helpers-7.0.0.tgz#61ee7ba5dba27d7cad72a13d46bec23c060b762e" + integrity sha512-tZKTMdhZvTy0KCEX5EGQQm1RHr7jUa36q/yax1baEA0yZapVYmu10yW7LTqijITgSq416gPVjrcexiA6y4pJlA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-proposal-async-generator-functions@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.0.0.tgz#5d1eb6b44fd388b97f964350007ab9da090b1d70" @@ -1579,6 +1586,14 @@ broccoli-file-creator@^1.1.1: broccoli-plugin "^1.1.0" mkdirp "^0.5.1" +broccoli-file-creator@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/broccoli-file-creator/-/broccoli-file-creator-2.1.1.tgz#7351dd2496c762cfce7736ce9b49e3fce0c7b7db" + integrity sha512-YpjOExWr92C5vhnK0kmD81kM7U09kdIRZk9w4ZDCDHuHXW+VE/x6AGEOQQW3loBQQ6Jk+k+TSm8dESy4uZsnjw== + dependencies: + broccoli-plugin "^1.1.0" + mkdirp "^0.5.1" + broccoli-funnel-reducer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/broccoli-funnel-reducer/-/broccoli-funnel-reducer-1.0.0.tgz#11365b2a785aec9b17972a36df87eef24c5cc0ea" @@ -1666,6 +1681,14 @@ broccoli-merge-trees@^3.0.0: broccoli-plugin "^1.3.0" merge-trees "^2.0.0" +broccoli-merge-trees@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/broccoli-merge-trees/-/broccoli-merge-trees-3.0.1.tgz#545dfe9f695cec43372b3ee7e63c7203713ea554" + integrity sha512-EFPBLbCoyCLdjJx0lxn+acWXK/GAZesXokS4OsF7HuB+WdnV76HVJPdfwp9TaXaUkrtb7eU+ymh9tY9wOGQjMQ== + dependencies: + broccoli-plugin "^1.3.0" + merge-trees "^2.0.0" + broccoli-middleware@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/broccoli-middleware/-/broccoli-middleware-1.2.1.tgz#a21f255f8bfe5a21c2f0fbf2417addd9d24c9436"