I wrote a utility library and I want to tree-shaking
them when my user publishes
their app.
In Webpack v4, you need to make your module ES6
to support tree-shaking
, but I also want to split my development build
and my production build
into different files.
What I want is exactly like react's NPM module:
// index.js
'use strict';
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react.production.min.js');
} else {
module.exports = require('./cjs/react.development.js');
}
This leads me questions.
If I make my utility modules all commonjs
, I will never get tree-shaking
, my app gets so huge.
If I make my utility modules all ES6 static export
, I will have to include development message
in production code
.
And publishing two modules (eg: my-utility
and my-utility-es
) will not helping, because in development, my code looks like this:
import { someFunc } from 'my-utility';
but in production code, I will have to change it to this:
import { someFunc } from 'my-utility-es';
How can I solve this problem?
To be more clear, my development build
and production build
contains different source code (eg: production build has stripped all error message).
So specify webpack mode isn't satisfying for me.
I've found out the answer by myself, I think the best way to do this is through babel macros
:
import { something } from 'myLibrary/macro';
// In webpack development:
// ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
// import { something } from 'myLibrary/development';
// In webpack production:
// ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
// import { something } from 'myLibrary/production';
My macro implementation:
import { createMacro } from 'babel-plugin-macros';
function macro({ references, state, babel }) {
state.file.path.node.body.forEach(node => {
if (node.type === 'ImportDeclaration') {
if (node.source.value.includes('myLibrary/macro')) {
if (process.env.NODE_ENV === 'production') {
node.source.value = 'myLibrary/module/production';
} else {
node.source.value = 'myLibrary/module/development';
}
}
}
});
return { keepImports: true };
}
export default createMacro(macro);
Here's the best solution I've found, without requiring the user to use Babel macros...
crazy-components
ComponentA
and ComponentB
// src/index.js
import React from 'react';
export function ComponentA(props) {
if (process.env.NODE_ENV !== 'production') {
console.log(`Rendering ComponentA with props ${props}`);
}
return <div>ComponentA message: {props.msg}</div>;
}
export function ComponentB(props) {
if (process.env.NODE_ENV !== 'production') {
console.log(`Rendering ComponentB with props ${props}`);
}
return <div>ComponentB message: {props.msg}</div>;
}
Be tree-shakable, so if user does import { ComponentA } from 'crazy-components'
, the code for ComponentB
does not end up in their bundle.
The logging code is stripped from production bundles.
CJS builds are output to /dist/cjs
, ESM builds to /dist/esm
. Files are called crazy-components.prod.min.js
and crazy-components.dev.js
.
Only the dev builds contains the logging code (not explaining how to do all this, if you're reading this, you probably already know).
// index.js
if (process.env.NODE_ENV === 'production') {
module.exports = require('./dist/cjs/crazy-components.min.js');
} else {
module.exports = require('./dist/cjs/crazy-components.js');
}
// es/index.js
import {
ComponentA as ComponentA_prod,
ComponentB as ComponentA_prod
} from '../dist/esm/crazy-components.prod.min.js';
import {
ComponentA as ComponentA_dev,
ComponentB as ComponentA_dev
} from '../dist/esm/crazy-components.dev.js';
export const ComponentA = process.env.NODE_ENV === 'production' ? ComponentA_prod : ComponentA_dev;
export const ComponentB = process.env.NODE_ENV === 'production' ? ComponentB_prod : ComponentB_dev;
package.json
:// package.json
{
"name": "crazy-components",
"version": "1.0.0",
"main": "index.js",
"module": "es/index.js",
"sideEffects": false
}
Node 12 (with flag) and Node 13+ support ES modules natively.
Add to package.json
:
"exports": {
".": {
"import": "./es/index.js",
"require": "./index.js"
},
"./es": "./es/index.js"
},
Add extra package.json
file in es
folder to flag contents of the folder as ESM to NodeJS:
// es/package.json
{
"type": "module"
}
Use rollup-plugin-copy to get Rollup to also copy this file into dist/esm
:
// rollup.config.js
import copy from 'rollup-plugin-copy';
/* ... other imports ... */
export default {
input: 'src/index.js',
/* ... other config ... */
plugins: [
/* ... other plugins ... */
copy({targets: [{src: 'es/package.json', dest: 'dist/esm'}]})
]
};
es/index.js
is created by hand, so if you later add ComponentC
, it also needs to be added to es/index.js
. It'd be ideal if there was a Rollup plugin to automate creation of es/index.js
, but I haven't found one.
Also, your mileage may vary. I've only just been trying this out today. It seems to work as you'd expect when the library is imported in a create-react-app app, but I've not tested it with hand-coded Webpack configs.
This approach should be generalisable to any library, not just React components, but I've not tried.
Any suggestions for improvements very welcome!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With