Resources
What & Link | Type |
---|---|
Bundlers Tooling Report - This is a great new resource (2020) for a comparison of common bundlers and build tools, especially based on supported feature sets. |
Comparison / Analysis |
Elevator Pitches: Bundlers | List / Comparison |
Parcel
*** --- WARNING!!! --- ***
When you are on the docs pages for Parcel, make sure you are on the correct version (V1 vs V2)!
And make sure you have the version of Parcel you mean to have (parcel --version
if you want to check).
Parcel Troubleshooting
- I'm not getting source maps!
- Things to make sure:
- Don't use
--no-source-maps
- Don't use
--experimental-scope-hoisting
- Don't use
- Things to make sure:
Webpack
Webpack Resources
What & Link | Type |
---|---|
Webpack Docs - "Getting Started" | Guide |
Flavio - "Introduction to Webpack" | Guide |
Andrew Welch - "An Annotated Webpack 4 Config for Frontend Web Development" | Guide |
Webpack Config File
If you want to use a config file with Webpack, you do so by making webpack.config.js
in the root of your project, and having a single export:
module.exports = {
// ...
};
For seeing all the options, and comments on what they do, reference the configuration page from the official docs.
You can use this interactive config builder tool, or this one, to help you get started.
Webpack Config File Intellisense / Autocomplete
If you are trying to get Intellisense to work in a webpack.config.js
file, add a type annotation so VSCode can understand the config object:
/** @type {import('webpack').Configuration} */
module.exports = {
mode: 'development',
// ...
}
See my blog post on the topic, for details.
Webpack Troubleshooting
- Issues building to a subdirectory, with relative URLs (for example, lots of
ChunkLoadError
messages, bundles not loading)- You probably want to use the
config.output.publicPath
config option, and make sure that it ends with a slash (so,= '/app/
, not= '/app
)
- You probably want to use the
- Using
ts-loader
causes all ofnode_modules
imports to be type-checked iftsconfig
hascheckJs
on, regardless of exclude settings.- One way around this is setting the
transpileOnly
option to true, and then runningtsc --noEmit
separately, for the type-checking part- This also has the benefit of speeding up the bundling process
- Since this is caused by the
"checkJs": true
setting for TSC (which I would call a bug, since it shouldn't ignore the exclude globs), you can work around it with a separate config just for WebPack / ts-loader.- So that you can leave that setting as-is in your main config, you can create a separate tsconfig file just for WebPack, and then pass it to
ts-loader
via theoptions
sub object.
- So that you can leave that setting as-is in your main config, you can create a separate tsconfig file just for WebPack, and then pass it to
- One way around this is setting the
Rollup
Rollup CLI
Although rollup supports a ton of arguments directly via CLI instead of through a config file, I generally would recommend sticking to a dedicated config file.
You can tell Rollup to explicitly read all values from the config file, and even switch between configs, by passing the config file name via -c
or -config
.
Example:
{
"scripts": {
"build": "rollup -c rollup.config.js",
"build-debug": "rollup -c rollup-debug.config.js"
}
}
Rollup Config File Intellisense / Autocomplete
If you are trying to get Intellisense to work in a rollup.config.js
file, add a type annotation so VSCode can understand the config object:
/** @type {import('rollup').RollupOptions} */
const config = {
//
};
export default config;
Or, for an array of configs:
/** @type {import('rollup').RollupOptions[]} */
const config = [
// ...
];
export default config;
Rollup - Multi-Entry and Nested ESM Output
By default, Rollup bundles your code into single file exports, to reduce the number of chunks. E.g. output.umd.js
and output.esm.js
.
However, for multiple reasons, a package author dev might prefer to output in a way that more closely mirrors the input directory - with a 1:1 mapping that allows for users of the generated code to use nested imports (e.g. import {myFunc} from 'my-lib/nested/funcs'
).
To do this in Rollup, you need to use the output.preserveModules
option, set to true
.
There is also a semi-related issue with Rollup - unless explicitly passed via
config.input
, Rollup will optimize out nestedindex.js
files. If you want those files / directories to be able to be drilled into from a consumer of your lib, you would need to explicitly mark them as entry point to Rollup. See Rollup Issue #3916 for details.
Vite
🚨 Given the speed of Vite's development, these notes are not likely to stay up-to-date. Always refer to Vite's official docs as a first resort.
Vite - Building a Library
Guide: Library Mode
Some random notes on building a library with Vite:
- You can use the
glob
package (or a similar one) to build thelib.entry
value, so you don't have to manually type out every entry point - You can use Rollup's
preserveModules
option to preserve the original file structure, as-is
Condensed example for a React library:
import react from '@vitejs/plugin-react';
import { resolve } from 'path';
import { defineConfig } from 'vite';
import dts from 'vite-plugin-dts';
import { glob } from 'glob';
export default defineConfig({
plugins: [
react(),
dts({
copyDtsFiles: true,
entryRoot: './nested-lib',
}),
],
build: {
minify: false,
lib: {
name: 'my-library',
entry: glob.sync(resolve(__dirname, 'client/**/*.{jsx,js,ts,tsx}')),
},
commonjsOptions: {
transformMixedEsModules: true,
},
rollupOptions: {
external: ['react'],
output: {
preserveModules: true,
preserveModulesRoot: 'nested-lib',
globals: {
react: 'React',
},
},
plugins: [],
},
outDir: 'dist',
emptyOutDir: true,
},
});
Vite - Subdirectory Deployment
If you know the subdirectory path in advance, you can use the Public Base Path feature. Provide a value for the base
config file option or --base
CLI config option and Vite will handle rewrites automatically.
However, if you want to create a build that works in any subdirectory, without knowing it in advance, it takes a bit more setup. A few changes to make:
- Use
base: ''
in yourvite.config.js
file, which forces relative paths in the HTML - Change any
src="/{path}"
attributes to use a relative path - In CSS files, and/or style blocks in SFCs, this gets a little more complicated
- Because CSS gets bundled into
{out}/dist/{bundle}.css
, any relative URLs point to/dist
, instead of the subdirectory root... This means that a CSS value ofurl('./images/my_img.png')
orurl('images/my_img.png')
will point to{out}/dist/images/my_img.png
instead of{out}/images/my_img.png
- Options are:
- Move CSS declaration from SFC into a CSS file that does not get bundled, but instead copied as-is, into root. This could be accomplished by using something like
{Project_Root}/public/global.css
- Run a transformer script over the generated CSS to replace the URLs
- Write a Vite plugin to fix this. Something like this
- Closely related issues: Issue #3650, #2394, #1115, and #762.
- Move CSS declaration from SFC into a CSS file that does not get bundled, but instead copied as-is, into root. This could be accomplished by using something like
- Because CSS gets bundled into
- I hacked together a quick script to fix paths in JS, HTML, and CSS dist files for subdirectory deployment - not very robust, but worked for my needs
Vite - Miscellaneous Troubleshooting
- The request url
"___"
is outside of Vite serving allow list.- Refer to docs for Vite's
server.fs.allow
array option - If this is for a local dependency that is symlinked in (e.g., via Yalc), you might need to also set the
resolve.preserveSymlinks
option totrue
- Refer to docs for Vite's
Reloading and Hot Module Replacement (HMR)
An important distinction to make; when considering the ability of a development environment to automatically refresh the browser / device when your local code changes, there are two popular mechanisms:
- Live Reloading:
- This typically reloads the entire app, regardless of what has changed. In practice, this means that editing one component 30 layers deep in your UI will cause the entire page to reload, and your app state will be destroyed / reset
- Some live-reloaders are at least able to avoid a full refresh if only CSS has changed, as that can easily be re-injected
- This is sometimes referred to as Hot Reloading, if discussed within the same context as Hot Module Reloading (HMR), but this seems confusing to me
- This typically reloads the entire app, regardless of what has changed. In practice, this means that editing one component 30 layers deep in your UI will cause the entire page to reload, and your app state will be destroyed / reset
- Hot Module Replacement:
- This only replaces the chunks of code that have actually changed, as opposed to the entire app. In practice, this often means that you can make changes to a component and only have that part of the app reload; everything else keeps it state.
- This is far more complex than whole-page reloads, but also greatly speeds up the development process when it can be used.
- HMR is only used for local development, and not actual production builds, so it often is implemented separately from the build task - e.g.
rollup
for builds, butsnowpack
for HMR and local dev.