Skip to content

Commit

Permalink
[FEAT] Adds an option to emit external babel helpers
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
pzuraq committed Nov 9, 2018
1 parent dffcd0f commit 95283a0
Show file tree
Hide file tree
Showing 10 changed files with 183 additions and 6 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ module.exports = {
'blueprints/*/index.js',
'config/**/*.js',
'tests/dummy/config/**/*.js',
'lib/**/*.js'
],
excludedFiles: [
'addon/**',
Expand Down
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ interface EmberCLIBabelConfig {
*/
'ember-cli-babel'?: {
includePolyfill?: boolean;
includeExternalHelpers?: boolean;
compileModules?: boolean;
disableDebugTooling?: boolean;
disablePresetEnv?: boolean;
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion ember-cli-build.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
});

Expand Down
66 changes: 63 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,19 +84,71 @@ 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;

// 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);
Expand All @@ -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;

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -188,6 +247,7 @@ module.exports = {
let userPostTransformPlugins = addonProvidedConfig.postTransformPlugins;

options.plugins = [].concat(
shouldIncludeHelpers && this._getHelpersPlugin(),
userPlugins,
this._getDebugMacroPlugins(config),
this._getEmberModulesAPIPolyfill(config),
Expand Down
23 changes: 23 additions & 0 deletions lib/inject-babel-helpers.js
Original file line number Diff line number Diff line change
@@ -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');
});
},
};
}
1 change: 0 additions & 1 deletion lib/relative-module-paths.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-env node */
'use strict';

const path = require('path');
Expand Down
31 changes: 30 additions & 1 deletion node-tests/addon-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {

Expand All @@ -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,
Expand Down Expand Up @@ -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 = {
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
14 changes: 14 additions & 0 deletions tests/unit/external-helpers-test.js
Original file line number Diff line number Diff line change
@@ -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();
});
});
23 changes: 23 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit 95283a0

Please sign in to comment.