Reputation: 4695
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.
Upvotes: 8
Views: 4175
Reputation: 181
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!
Upvotes: 3
Reputation: 4695
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);
Upvotes: 2
Reputation: 7358
All you need to solve this problem is to use mode
. See Specify the Mode.
Since webpack v4, specifying mode automatically configures DefinePlugin for you:
webpack.prod.js
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
mode: 'production',
});
They mention React by name:
If you're using a library like react, you should actually see a significant drop in bundle size after adding this plugin.
Upvotes: -2