Skip to content

Commit

Permalink
Introduce Babel transform to convert 'lodash-es' imports to 'lodash' (A…
Browse files Browse the repository at this point in the history
…utomattic#22187)

* Introduce Babel transform to convert 'lodash-es' imports to 'lodash'

Some 3rd party libraries like Redux Form or React Redux internally import Lodash
functions from `lodash-es`. Because Calypso code imports from `lodash`, this leads
to code duplication.

This patch introduces a Babel transform to transform these imports to `lodash` and
activates the transform on the `redux-form` and `react-redux` libraries. Because
these libraries provide ES6 versions of their exports, we can use smart transform
like this one.

* Rewrite tests to not use snapshots

* Add the lodash-es transform to server build and fix builds in test env

Remove the babel-lodash-es transform from babelrc when building in
NODE_ENV=test environment. In that case, the transform is already done
in the Webpack loader. Having the transform in babelrc is useful only
for Jest tests.

Also, adds the transform to the server build and removes lodash vs.
lodash-es duplication from the server bundle.
  • Loading branch information
jsnajdr authored Feb 8, 2018
1 parent af6ff0a commit e58328d
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 1 deletion.
1 change: 1 addition & 0 deletions .babelrc
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"env": {
"test": {
"plugins": [
"./server/bundler/babel/babel-lodash-es",
[ "transform-builtin-extend", {
"globals": [ "Error" ]
} ]
Expand Down
18 changes: 18 additions & 0 deletions server/bundler/babel/babel-lodash-es/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
Lodash Babel Transform
======================

This Babel transform transforms imports from the `lodash-es` module, commonly found in
third party NPM packages, into imports from `lodash`:

```js
import get from 'lodash-es/get'
```

is transformed into:

```js
import { get } from 'lodash'
```

This transform avoid duplication of the Lodash library and makes sure that all imports
from all packages are satisfied by the single global `lodash` module.
54 changes: 54 additions & 0 deletions server/bundler/babel/babel-lodash-es/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/** @format */

const types = require( 'babel-types' );

module.exports = function() {
return {
visitor: {
ImportDeclaration( path ) {
const { source } = path.node;

// Transform any import from 'lodash-es' (exact match, no submodule) to 'lodash'
const fullMatch = source.value.match( /^lodash-es$/ );
if ( fullMatch ) {
source.value = 'lodash';
return;
}

// Transform default import from a 'lodash-es' submodule to a a named import from 'lodash'
// Example:
// In: import theGet from 'lodash-es/get'
// Out: import { get as theGet } from 'lodash'
const subMatch = source.value.match( /^lodash-es\/(.*)/ );
if ( subMatch ) {
const { specifiers } = path.node;
// If there is anything else than a single default import, throw an error. Such an
// import is not valid. Example: import { get } from 'lodash-es/get'
// There is only the default export in 'lodash-es/get' and no named one.
if (
! specifiers ||
specifiers.length !== 1 ||
specifiers[ 0 ].type !== 'ImportDefaultSpecifier'
) {
throw path.buildCodeFrameError(
'babel-lodash-es: Could not transform a non-default import from lodash-es/submodule'
);
}

// Transforms
// `import theGet from 'lodash-es/get'`
// to
// `import { get as theGet } from 'lodash'`
const localIdentifier = specifiers[ 0 ].local;
const importedIdentifier = types.identifier( subMatch[ 1 ] );
const specifier = types.importSpecifier( localIdentifier, importedIdentifier );
const declaration = types.importDeclaration(
[ specifier ],
types.stringLiteral( 'lodash' )
);
path.replaceWith( declaration );
}
},
},
};
};
39 changes: 39 additions & 0 deletions server/bundler/babel/babel-lodash-es/test/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/** @format */

/**
* External dependencies
*/
const babel = require( 'babel-core' );

describe( 'babel-lodash-es', () => {
function transform( code ) {
return babel.transform( code, {
plugins: [ require( '..' ) ],
} ).code;
}

test( 'should transform named import from top-level package', () => {
const code = transform( "import { get } from 'lodash-es';" );
expect( code ).toBe( "import { get } from 'lodash';" );
} );

test( 'should transform default import from top-level package', () => {
const code = transform( "import _ from 'lodash-es';" );
expect( code ).toBe( "import _ from 'lodash';" );
} );

test( 'should transform default import from a submodule', () => {
const code = transform( "import get from 'lodash-es/get'" );
expect( code ).toBe( "import { get } from 'lodash';" );
} );

test( 'should transform aliased default import from a submodule', () => {
const code = transform( "import theGet from 'lodash-es/get';" );
expect( code ).toBe( "import { get as theGet } from 'lodash';" );
} );

test( 'should report error on a non-default import from a submodule', () => {
const code = "import { get } from 'lodash-es/get'";
expect( () => transform( code ) ).toThrow( /Could not transform a non-default import/ );
} );
} );
2 changes: 1 addition & 1 deletion test/client/jest.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"<rootDir>/client/"
],
"testEnvironment": "node",
"transformIgnorePatterns": ["node_modules[\\/\\\\](?!redux-form|lodash-es)"],
"transformIgnorePatterns": ["node_modules[\\/\\\\](?!redux-form)"],
"testMatch": [ "<rootDir>/client/**/test/*.js?(x)" ],
"testURL": "https://example.com",
"setupTestFrameworkScriptFile": "<rootDir>/test/client/setup-test-framework.js",
Expand Down
12 changes: 12 additions & 0 deletions webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ const babelPresetEnv = _.find( babelConfig.presets, preset => preset[ 0 ] === 'e
babelPresetEnv[ 1 ].modules = false;
_.remove( babelConfig.plugins, elem => elem === 'add-module-exports' );

// remove the babel-lodash-es plugin from env.test -- it's needed only for Jest tests.
// The Webpack-using NODE_ENV=test build doesn't need it, as there is a special loader for that.
_.remove( babelConfig.env.test.plugins, elem => /babel-lodash-es/.test( elem ) );

/**
* This function scans the /client/extensions directory in order to generate a map that looks like this:
* {
Expand Down Expand Up @@ -111,6 +115,14 @@ const webpackConfig = {
exclude: /node_modules[\/\\](?!notifications-panel)/,
loader: [ 'happypack/loader' ],
},
{
test: /node_modules[\/\\](redux-form|react-redux)[\/\\]es/,
loader: 'babel-loader',
options: {
babelrc: false,
plugins: [ path.join( __dirname, 'server', 'bundler', 'babel', 'babel-lodash-es' ) ],
},
},
{
test: /extensions[\/\\]index/,
exclude: path.join( __dirname, 'node_modules' ),
Expand Down
12 changes: 12 additions & 0 deletions webpack.config.node.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ const commitSha = process.env.hasOwnProperty( 'COMMIT_SHA' ) ? process.env.COMMI
const babelConfig = JSON.parse( fs.readFileSync( './.babelrc', { encoding: 'utf8' } ) );
_.remove( babelConfig.plugins, elem => elem === 'add-module-exports' );

// remove the babel-lodash-es plugin from env.test -- it's needed only for Jest tests.
// The Webpack-using NODE_ENV=test build doesn't need it, as there is a special loader for that.
_.remove( babelConfig.env.test.plugins, elem => /babel-lodash-es/.test( elem ) );

/**
* This lists modules that must use commonJS `require()`s
* All modules listed here need to be ES5.
Expand Down Expand Up @@ -121,6 +125,14 @@ const webpackConfig = {
exclude: /(node_modules|devdocs[\/\\]search-index)/,
loader: [ 'happypack/loader' ],
},
{
test: /node_modules[\/\\](redux-form|react-redux)[\/\\]es/,
loader: 'babel-loader',
options: {
babelrc: false,
plugins: [ path.join( __dirname, 'server', 'bundler', 'babel', 'babel-lodash-es' ) ],
},
},
],
},
resolve: {
Expand Down

0 comments on commit e58328d

Please sign in to comment.