Webpack tree shaking not working between packages - javascript

Good evening!
I have been trying for a few days to get tree shaking between different packages to work.
Before going further, I have created a minimum repro that I will explain throughout this post: https://github.com/Apidcloud/tree-shaking-webpack
I have also opened an issue on webpack repo: https://github.com/webpack/webpack/issues/8951
For simplicity sake, the example just uses webpack. Babel is not used.
The above example has two packages, both with their respective bundle:
core - exports 2 functions, cube and unusedFn
consumer - imports cube from core and exports its own function, consumerFn
Core package
Note that square function is not exported in the index.js file. It's a way to know that tree shaking is indeed working within core at least, as it's not included in the final bundle (which is correct).
Consumer package
As you can see, only cube is being imported from core. It then exports its own function (consumerFn) consuming cube.
Problem
The problem is that the consumer bundle is including everything from the core bundle. That is, it's including unusedFn when it shouldn't, resulting in a bigger bundle.
Ultimately, the goal is to do the same in a monorepo with multiple packages. There's no point on having them if each package is bundling the everything from the others. The goal is to bundle only what's necessary for each package.
Using optimizationBailout I can see that ModuleConcatenation plugin is issuing some warning messages. I also used --verbose flag:
Here's my webpack.config.js:
const path = require('path');
module.exports = {
mode: 'production',
entry: {
core: './src/index.js',
consumer: './consumer/index.js'
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist'),
// for simplicity sake I removed the UMD specifics here.
// the problem is the same, with or without it.
},
optimization: {
usedExports: true,
sideEffects: true
},
stats: {
// Examine all modules
maxModules: Infinity,
// Display bailout reasons
optimizationBailout: true
}
};
I also have "sideEffects": false in the package.json.
I went through webpack's guide too, but I'm not sure what is missing.
Related issues:
webpack-3-babel-and-tree-shaking-not-working
webpack-including-unused-exports-in-final-bundle-not-tree-shaking

Related

Module not found: Error: You attempted to import babel-preset which falls outside of the project src/ directory

I'm developing an application created using create-react-app
But then I needed to use mediainfojs library, this library requires wasm files, and based on what I understood I couldn't add it using create-react-app, I had to eject it.
After ejecting it, I went to mediainfo information on how to add the wasm on the webpack
They use the CopyPlugin, but then when I tried to do that it complained about the versions of my webpack (4) and the CopyPlugin.... so, I decided to migrate to webpack 5
That is when the pain starts... after follow their migration tutorial and do a bunch of modifications on my webpack.config I got to the following error while runing yarn build:
Module not found: Error: You attempted to import /MyWorkspace/project/node_modules/babel-preset-react-app/node_modules/#babel/runtime/helpers/esm/asyncToGenerator which falls outside of the project src/ directory. Relative imports outside of src/ are not supported.
The only place calling this babel-preset-react-app are in the configuation
Here:
{
test: /\.(js|mjs|jsx|ts|tsx)$/,
include: paths.appSrc,
loader: require.resolve("babel-loader"),
options: {
customize: require.resolve(
"babel-preset-react-app/webpack-overrides"
),
And here:
{
test: /\.(js|mjs)$/,
exclude: /#babel(?:\/|\\{1,2})runtime/,
loader: require.resolve("babel-loader"),
options: {
babelrc: false,
configFile: false,
compact: false,
presets: [
[
require.resolve("babel-preset-react-app/dependencies"),
{ helpers: true },
],
],
cacheDirectory: true,
cacheCompression: isEnvProduction,
// If an error happens in a package, it's possible to be
// because it was compiled. Thus, we don't want the browser
// debugger to show the original code. Instead, the code
// being evaluated would be much more helpful.
sourceMaps: false,
},
},
I have looked into similar issues reported here, but mostly of them seem to be related to either static files being dynamically imported or imports referencing ".." dir after the project directory
The full webpack config file is here
I'm probably missing something very silly, I'd be glad if someone can point it out.
I had a similar challenge and I was able to fix this by adding these definitions at the top of my webpack.config file
const babelRuntimeEntry = require.resolve('babel-preset-react-app');
const babelRuntimeEntryHelpers = require.resolve(
'#babel/runtime/helpers/esm/assertThisInitialized',
{ paths: [babelRuntimeEntry] }
);
const babelRuntimeRegenerator = require.resolve('#babel/runtime/regenerator', {
paths: [babelRuntimeEntry]
});
Then where you have the ModuleScopePlugin in the resolve.plugins
update it to be
new ModuleScopePlugin(paths.appSrc, [
paths.appPackageJson,
babelRuntimeEntry,
babelRuntimeEntryHelpers,
babelRuntimeRegenerator])
I'm also attempting to upgrade an ejected CRA project to Webpack 5. I was able to move forward using babel-preset-react-app-webpack-5, only to encounter the next CRA-related issue.
Be sure to replace calls like require.resolve("babel-preset-react-app/dependencies") with require.resolve("babel-preset-react-app-webpack-5/dependencies").
Also, be aware the package does not appear to be production-ready, but my own project is still in early development.
I had this issue with a few other babel packages after trying to upgrade an ejected CRA app to webpack v5. I tried many different approaches some of which worked in dev but not in prod and vice versa. I found this comment in the storybook github and it was the only thing that that seemed to work in all scenarios for me.
It's kinda annoying, but by simply moving the offending packages from devDependencies in my package.json to dependencies, it seems to fix the issue. I could spend more time trying to figure out why that fixes it, but I'll leave that to someone with more free time. :)

Why webpack doesn't tree-shake the lodash when using "import * as _"?

I am learning about tree-shaking with a webpack 4/React application that uses Lodash.
At first, my Lodash usage looked like this:
import * as _ from "lodash";
_.random(...
I soon learned, via the BundleAnalyzerPlugin, that the entirety of Lodash was being included in both dev and prod builds (527MB).
After googling around I realized that I needed to use a specific syntax:
import random from "lodash/random";
random(...
Now, only random and it's dependencies are correctly included in the bundle, but I'm still a little confused.
If I need to explicitly specify functions in my import statement, then what role is the tree-shaking actually playing?
The BundleAnalyzerPlugin isn't showing a difference in payload size when comparing between dev and production mode builds (it's the correct small size in both, but I thought that tree-shaking only took place with production builds?).
I was under the impression that TreeShaking would perform some sort of static code analysis to determine which parts of the code were actually being used (perhaps based on function?) and clip off the unused bits.
Why can't we always just use * in our import and rely on TreeShaking to figure out what to actually include in the bundle?
In case it helps, here is my webpack.config.js:
const path = require("path");
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
module.exports = {
entry: {
app: ["babel-polyfill", "./src/index.js"]
},
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: "static",
openAnalyzer: false
})
],
devtool: "source-map",
output: {
filename: "[name].js",
path: path.resolve(__dirname, "dist"),
chunkFilename: "[name].bundle.js",
publicPath: ""
},
module: {
rules: [
{
test: /\.js$/,
loader: "babel-loader",
include: /src/,
options: {
babelrc: false,
presets: [
[
"env",
{
targets: {
browsers: ["last 2 Chrome versions"]
}
}
],
"#babel/preset-env",
"#babel/preset-react"
],
plugins: ["syntax-dynamic-import"]
}
},
{
test: /\.(ts|tsx)$/,
use: [
{
loader: require.resolve("ts-loader"),
options: {
compiler: require.resolve("typescript")
}
}
]
}
]
},
resolve: {
symlinks: false,
extensions: [".js", ".ts", ".tsx"],
alias: {
react: path.resolve("./node_modules/react")
}
}
};
I'm invoking webpack with webpack --mode=development and webpack --mode=production.
All two existing answers are wrong, webpack do treeshake import *, however that only happens when you're using a esmodule, while lodash is not. The correct solution is to use lodash-es
Edit: this answer only applies to webpack4, while webpack 5 supported a limited subset of tree shaking for commonjs, but I haven't tested it myself
Actually, it is not related to the Webpack ability to tree-shake. base on Webpack docs about tree-shaking
The new webpack 4 release expands on this capability with a way to provide hints to the compiler via the "sideEffects" package.json property to denote which files in your project are "pure" and therefore safe to prune if unused
When you set the "sideEffects: false on your package.json based on the linked docs:
All the code noted above does not contain side effects, so we can simply mark the property as false to inform webpack that it can safely prune unused exports.
If you have some files or packages which you know they are pure add them to sideEffects to prune it if unused. There are some other solutions to do tree-shaking that I proffer to read the whole article on Webpack docs.
One of the manual ways are using direct importing like below:
import get from 'lodash/get';
That Webpack understands add just get from the whole lodash package. another way is destructing importing that needs some Webpack optimization for to tree-shaking, so you should import like below:
import { get } from 'lodash';
Also, another tricky way is just to install a specific package, I mean:
yarn add lodash.get
OR
npm install --save lodash.get
Then for import just write:
import get from 'lodash.get';
Definitely, it is not tree-shaking, it is a tight mindset development, but it causes you just add what you want.
BUT
YOU DON'T DO ANYTHING OF ABOVE SOLUTIONS, you just add the whole package by writing import * as _ from "lodash"; and then use _.random or any function and expect Webpack understand you wanna the tree-shaking be happening?
Surely, the Webpack works well. you should use some configs and coding style to see the tree-shaking happens.
If you're already using Babel, the easiest method to properly tree shake lodash is to use the official babel-plugin-lodash by the lodash team.
This uses Babel to rewrite your lodash imports into a more tree-shakeable form. Doing this dropped my team's bundle size by ~32kB (compressed) with less than 5 minutes of effort.

How to get a filename renamed by Webpack?

I'm using a webassembly file compiled by Emscripten in my project.
And since my project uses Webpack, it renames all files, then the Emscripten module can't find more the webassembly file.
Then I need to get the new filename of the webassembly file to can load it.
I found this workaround, but I want a better solution, because I don't want do change the webpack.config.js with configurations about the .wasm files.
Explain the context: I have a project called bursh that it uses Webpack and imports a module called scissors which it has the webassembly files. So I'm looking for a solution which doesn't need to update the configurations, because of the isolation of responsibilities - doesn't make sense to set configurations at brush for some reason by scissors
From your description you might want to look into copy-webpack-plugin. Webpack normally bundles multiple files into one or several larger files, but if you additionally want to copy files into your build, this plugin can do just that.
I resolved the problem. My solution is adding Webpack at scissors in order to set the configurations in this project.
const path = require('path')
const rules = [
{
loader: 'file-loader',
test: /huffman\.wasm$/,
type: 'javascript/auto',
},
]
rules.concat()
module.exports = {
devtool: 'source-map',
entry: './src/index.js',
module: {
rules,
},
node: {
fs: 'empty',
},
output: {
filename: 'scissors.js',
libraryTarget: 'commonjs2',
path: path.join(__dirname, './dist'),
sourceMapFilename: 'scissors.js.map',
},
}
I don't know if this is the best solution, because now I have a Webpack in brush project and also in scissors project, then it will increase complexity in my code... But in this way I can keep the isolation of responsibilities.

webpack & using node modules in an isomorphic package

I am building an isomorphic package where I am using a flag in various files to detect if we are in the browser or in node. If in node, I require an internal package ie if (isNode) { require("/.nodeStuff) } that has as one of its dependencies the fs module. However, webpack does not like this for obvious reasons. Is there any type of module-based webpack config file that I can configure to ignore the node-based requires entirely so that this does not happen?
First option
As stated in the docs, in order to solve this isomorphic problem you could simply run two builds, one for each environment (node and web). The guide can be found here. Keep in mind you should probably mock any built ins in the clientConfig by adding this block
node: { fs: 'empty',//any other node lib used }. That way webpack will not complain and since your client code will be under the !IS_NODE condition the empty fs will never be used.
Although this is a solid solution you end up with 2 bundles and you need of course a way to distribute them to the correct platform each time.
Second way
This solution is based on the not very well known __non_webpack_require__ function. This is a webpack specific function that will instruct the parser to avoid bundling this module that is being requested and assume that a global require function is available. This is exactly what happens while running in node instead of a browser.
//webpack.config.js
{
mode: "development",
entry: './index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
},
node: false
}
// nodeStuff.js
const fs = __non_webpack_require__('fs'); //this will be transformed to require('fs')
fs.writeFileSync('some','thing)
That way since nodeStuff.js will only be required under the IS_NODE condition, the native require will be available.
I would suggest to use __non_webpack_require__ on native libraries only, that you are sure that will be available!

How to bundle js library for use in browser with webpack?

I'm trying to create a minified version of a js library with webpack.
The library consists of one main function with prototypes that is exported and of several other functions it depends on that are imported in the file of the main function. This works without bundling and I assume that this file should the entry point for webpack.
I aim to bundle it into some mylib.min.js to be able access it in the browser like I would use jQuery or similar libraries. So I don't want to bundle the whole web app, just the JS library I wrote.
I'm not really getting along with it, since all tutorials show how to bundle the whole web app. My questions are:
how do I have to export the main function of the library to be able to access it in the browser?
how do I need to configure webpack?
how should I include and access the bundle in the browser?
If you could recommend any example (like tutorial, gitub repository that does this, ...) I would be happy! Any suggestions welcome!
Have you look at the documentation on the webpack website?
Here an example
For widespread use of the library, we would like it to be compatible in different environments, i.e. CommonJS, AMD, Node.js and as a global variable. To make your library available for consumption, add the library property inside output:
webpack.config.js
var path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
- filename: 'webpack-numbers.js'
+ filename: 'webpack-numbers.js',
+ library: 'webpackNumbers'
},
externals: {
lodash: {
commonjs: 'lodash',
commonjs2: 'lodash',
amd: 'lodash',
root: '_'
}
}
};
If you have any other question about that specific documentation just google webpack js authoring libraries. you'll be redirect to the good website. Website are subject to change pattern.

Categories