Tree shaking does not work in vite library mode - vite

I am building a library with vite library mode, the problem is the bundler is not tree shakable.
There are some related topics here, here and maybe here also.
So, anyone has experience with this?

I believe this has to do with the way esbuild bundles under the hood. It's a little different than the way babel has done it with their more established plugin eco-system.
Some of your code that you would expect to the tree-shaken by default might not be marked as /* #__PURE__ */ as you might expect.
esbuild issue
esbuild docs
The solution that worked for me was:
Mark all functions that are able to be dropped if unused, tree-shaken, with /* #PURE */. (use caution)
// src/icon.tsx
/* #__PURE__ */
export const Icon = React.forwardRef<SVGSVGElement, IconProps>(
(props, forwardedRef) => (
<svg {...props} ref={forwardedRef}>
<path
d="M14.5"
fill={props.color}
/>
</svg>
),
)
Preserve modules in your build step:
// vite.config.ts
rollupOptions: {
output: {
preserveModules: true,
},
},

Related

Unable to get `ReactComponent` export of #svgr/webpack

I am trying to get the same import statements going for both a Vite and Webpack setup (used by Storybook) and I am failing to get it to work for Webpack as advertised.
According to the docs for #svgs/webpack, version 5.5, this should work:
// Does not work (undefined)
import {ReactComponent as UilPlus} from '#iconscout/unicons/svg/line/plus.svg'
But it does not. There is only the default export:
// Works
import UilPlus from '#iconscout/unicons/svg/line/plus.svg'
How can I fix this? I have even tried explicitly using the namedExport option, but it does not take effect.
Config:
use: [
{
loader: '#svgr/webpack',
options: { namedExport: 'ReactComponent' }
},
$ jq .version node_modules/\#svgr/webpack/package.json
"5.5.0"
After reading the source code for v5.5 of #svgr/webpack, #svgr/core and #svgr/babel-plugin-transform-svg-component and reading up on how loaders work, things finally made sense.
Turns out there was a crucial bit missing: the example showing use of ReactComponent has an extra loader added for the svg processing. I did not understand how a loader added later could affect something earlier. That was until I read the loader docs for Webpack 4 which explains how loaders are applied Right to Left.
Once that was out of the way, it was now clear that the svgr plugin somehow sees that the input it receives is from another loader and adjusts it behavior accordingly. That is exactly what happens in #svgr/webpack in line 32 to 41 and you can see the expected output in the snapshot test:
...
export default \\"data:image/svg+xml;base64,PD94bWwg..."
export { SvgIcon as ReactComponent };"```
The webpack module actually only does the detection and passes that info on to #svgr/core, whereas the core module is using babel-plugin-transform-svg-component to actually change the output based on the presence of a previous export.
As said in doc of #svgr/webpack, you should add url-loader or file-loader
https://github.com/gregberge/svgr/tree/main/packages/webpack
{
test: /\.svg$/,
use: ['#svgr/webpack', 'url-loader'],
}
then you can use it in your code
import starUrl, { ReactComponent as Star } from './star.svg'
const App = () => (
<div>
<img src={starUrl} alt="star" />
<Star />
</div>
)

How to import a node module inside an angular web worker?

I try to import a node module inside an Angular 8 web worker, but get an compile error 'Cannot find module'. Anyone know how to solve this?
I created a new worker inside my electron project with ng generate web-worker app, like described in the above mentioned ng documentation.
All works fine until i add some import like path or fs-extra e.g.:
/// <reference lib="webworker" />
import * as path from 'path';
addEventListener('message', ({ data }) => {
console.log(path.resolve('/'))
const response = `worker response to ${data}`;
postMessage(response);
});
This import works fine in any other ts component but inside the web worker i get a compile error with this message e.g.
Error: app/app.worker.ts:3:23 - error TS2307: Cannot find module 'path'.
How can i fix this? Maybe i need some additional parameter in the generated tsconfig.worker.json?
To reproduce the error, run:
$ git clone https://github.com/hoefling/stackoverflow-57774039
$ cd stackoverflow-57774039
$ yarn build
Or check out the project's build log on Travis.
Note:
1) I only found this as a similar problem, but the answer handles only custom modules.
2) I tested the same import with a minimal electron seed which uses web workers and it worked, but this example uses plain java script without angular.
1. TypeScript error
As you've noticed the first error is a TypeScript error. Looking at the tsconfig.worker.json I've found that it sets types to an empty array:
{
"compilerOptions": {
"types": [],
// ...
}
// ...
}
Specifying types turns off the automatic inclusion of #types packages. Which is a problem in this case because path has its type definitions in #types/node.
So let's fix that by explicitly adding node to the types array:
{
"compilerOptions": {
"types": [
"node"
],
// ...
}
// ...
}
This fixes the TypeScript error, however trying to build again we're greeted with a very similar error. This time from Webpack directly.
2. Webpack error
ERROR in ./src/app/app.worker.ts (./node_modules/worker-plugin/dist/loader.js!./src/app/app.worker.ts)
Module build failed (from ./node_modules/worker-plugin/dist/loader.js):
ModuleNotFoundError: Module not found: Error: Can't resolve 'path' in './src/app'
To figure this one out we need to dig quite a lot deeper...
Why it works everywhere else
First it's important to understand why importing path works in all the other modules. Webpack has the concept of targets (web, node, etc). Webpack uses this target to decide which default options and plugins to use.
Ordinarily the target of a Angular application using #angular-devkit/build-angular:browser would be web. However in your case, the postinstall:electron script actually patches node_modules to change that:
postinstall.js (parts omitted for brevity)
const f_angular = 'node_modules/#angular-devkit/build-angular/src/angular-cli-files/models/webpack-configs/browser.js';
fs.readFile(f_angular, 'utf8', function (err, data) {
var result = data.replace(/target: "electron-renderer",/g, '');
var result = result.replace(/target: "web",/g, '');
var result = result.replace(/return \{/g, 'return {target: "electron-renderer",');
fs.writeFile(f_angular, result, 'utf8');
});
The target electron-renderer is treated by Webpack similarily to node. Especially interesting for us: It adds the NodeTargetPlugin by default.
What does that plugin do, you wonder? It adds all known built in Node.js modules as externals. When building the application, Webpack will not attempt to bundle externals. Instead they are resolved using require at runtime. This is what makes importing path work, even though it's not installed as a module known to Webpack.
Why it doesn't work for the worker
The worker is compiled separately using the WorkerPlugin. In their documentation they state:
By default, WorkerPlugin doesn't run any of your configured Webpack plugins when bundling worker code - this avoids running things like html-webpack-plugin twice. For cases where it's necessary to apply a plugin to Worker code, use the plugins option.
Looking at the usage of WorkerPlugin deep within #angular-devkit we see the following:
#angular-devkit/src/angular-cli-files/models/webpack-configs/worker.js (simplified)
new WorkerPlugin({
globalObject: false,
plugins: [
getTypescriptWorkerPlugin(wco, workerTsConfigPath)
],
})
As we can see it uses the plugins option, but only for a single plugin which is responsible for the TypeScript compilation. This way the default plugins, configured by Webpack, including NodeTargetPlugin get lost and are not used for the worker.
Solution
To fix this we have to modify the Webpack config. And to do that we'll use #angular-builders/custom-webpack. Go ahead and install that package.
Next, open angular.json and update projects > angular-electron > architect > build:
"build": {
"builder": "#angular-builders/custom-webpack:browser",
"options": {
"customWebpackConfig": {
"path": "./extra-webpack.config.js"
}
// existing options
}
}
Repeat the same for serve.
Now, create extra-webpack.config.js in the same directory as angular.json:
const WorkerPlugin = require('worker-plugin');
const NodeTargetPlugin = require('webpack/lib/node/NodeTargetPlugin');
module.exports = (config, options) => {
let workerPlugin = config.plugins.find(p => p instanceof WorkerPlugin);
if (workerPlugin) {
workerPlugin.options.plugins.push(new NodeTargetPlugin());
}
return config;
};
The file exports a function which will be called by #angular-builders/custom-webpack with the existing Webpack config object. We can then search all plugins for an instance of the WorkerPlugin and patch its options adding the NodeTargetPlugin.

Webpack fails with Node FFI and Typescript - dynamic require error

In a simple Typescript program I require Node FFI with
import * as Electron from 'electron';`
import * as ffi from 'ffi';`
and then
mylib = ffi.Library('libmoi', {
'worker': [ 'string', [ 'string' ] ],
'test' : [ 'string', [] ]
} );
Linking that up via webpack yields
WARNING in ./~/bindings/bindings.js
Critical dependencies:
76:22-40 the request of a dependency is an expression
76:43-53 the request of a dependency is an expression
# ./~/bindings/bindings.js 76:22-40 76:43-53
The problem seems to be that FFI has a dynamic require and the fix seems to be to apply webpack.ContextReplacementPlugin in the webpack.config.js file.
This is a bit out of my reach, but an example for an Angular case is:
plugins: [
new webpack.ContextReplacementPlugin(
// The (\\|\/) piece accounts for path separators in *nix and Windows
/angular(\\|\/)core(\\|\/)(esm(\\|\/)src|src)(\\|\/)linker/,
root('./src') // location of your src
)
]
Any idea how to do this for FFI?
Here is the answer: github issue comment on the Johnny-Five repo
Quoting from brodo's answer, this is what you do to stop webpack getting snarled up with "bindings" and similar:
... the webpack config looks like this:
module.exports = {
plugins: [
new webpack.ContextReplacementPlugin(/bindings$/, /^$/)
],
externals: ["bindings"]
}
I also had a similar issue, somehow, I managed to resolve it. I will first explain my understanding.
Main work of webpack is to bundle the separate code file into one file, by default it bundles all the code that is referenced in its tree.
Generally two types of node_modules:
To be used on browser side(angular, rxjs etc)
To be used on nodejs side(express, ffi etc)
It is safer to bundle browser side node_module but not safer to bundle node side node_module because they are not designed like that So the solution is below two steps:
Give appropriate target(node, electron etc) in webpack.config.js file e.g "target":'electron-renderer' by default it is browser
Declare node_side module as external dependency in your webpack.config.js file e.g.
"externals": {
"bindings": "require('bindings')",
"ffi": "require('ffi')"
}

Inherit bootstrap class with SASS in node

I want to add Bootstrap CSS to my own sub-elements by using SASS inheritance:
nav > a {
#extend: .nav-item;
#extend: .nav-link;
}
I use Node with webpack for bundling. And I've installed the bootstrap-sass but I can't seem to get the #import 'bootstrap' to work. All I get is File to import not found or unreadable: bootstrap. The sass part of the webpack code is:
module: {
loaders: [
....,
{
test: /\.scss$/,
loader: ExtractTextPlugin.extract('css!sass'),
}],
I guess this must be something trivial that I've missed. It's not entirely surprising that SASS doesn't have access to the library but I haven't found any good hints on how to provide the library directly to SASS.
I literally have no what you have done so far for setup and other things. In simple english, if you are using SASS files downloaded from bootstrap's official website, you can use their mixins, variables and extend code in your own code file. Though it needs proper project setup for files and import them in a proper way.
In the shared code, your syntax for #extend appears to be wrong. I have shown a dummy code snippet for demo purpose.
// code already written inside Bootstrap source file.
.nav-item {
background:red;
}
.nav-link {
color: #fff;
}
// your code
.nav > a {
#extend .nav-item;
#extend .nav-link;
}
You can use http://www.sassmeister.com/website for trial and error.
This is what it looks like, when compiled.

Webpack: expressing module dependency

I'm trying to require the bootstrap-webpack module in my webpacked application.
It appears to need jQuery, since the bundled javascript then throws the following:
Uncaught ReferenceError: jQuery is not defined
How do I go about specifying to webpack that jQuery is a dependency for the bootstrap-webpack module, to fix this issue? It feels like it should be trivial, but I've been struggling to figure it out.
I've tried adding:
"jquery": "latest"
to the dependecies in the bootstrap-webpack's package.json, but this didn't work. The documentation is incomplete, and I can't seem to find much about this issue. It should be trivial, right? Help!
There are two possible solutions:
Use the ProvidePlugin: It scans the source code for the given identifier and replaces it with a reference to the given module, just like it has been required.
// webpack.config.js
module.exports = {
...
plugins: [
new webpack.ProvidePlugin({
$: "jquery",
jQuery: "jquery"
})
]
};
Use the imports-loader: It provides the possibility to prepend preparations like require() statements.
// webpack.config.js
{
...
module: {
loaders: [
{ test: require.resolve("jquery"), loader: "imports?jQuery=jquery" }
]
}
}
In that case you need to run npm install imports-loader --save before.
Via this github issue.
Install expose-loader and add require('expose?$!expose?jQuery!jquery'); to your main entry point just before you require webpack-bootstrap.
This will set jQuery on the window so any file can get at it. Be careful with this method all files will then have access to that version of jQuery regardless of whether it was explicitly required.

Resources