This major release transitions bud.js to ESM. It also provides some cool new features (like importing from remote sources), but we’ll mainly be talking about the new ESM syntax.
Read the official announcement on bud.js.org.
Introduction
The transition to EcmaScript modules is causing a lot of division and drama in the JS world, at the moment.
Having just finished transitioning all of the nearly 50 packages that make up the bud.js monorepo to use
ESM I can say in all honesty that I totally get it. It was a very frustrating experience.
Which is to say, whatever problems come up with this release, let’s work together as a community to help one another
get through it. There will be problems. I hope not a lot, but this is the type of transition that happens every twenty
years, maybe. And I can assure you, on the other side of throwing require
out the window, it feels good to be on the
future-facing side of a great schism like this one.
I hope the appeal for solidarity wasn’t too spooky because the good news is that, for the vast majority of
bud.js users, not much is required here.
See “that sindresorhus README” for more context.
Upgrade guide
There are three upgrade paths available to JS users:
- Keep your config file the same and use the
.cjs
extension - Update your config file to use ESM syntax and use the
.mjs
extension - Transition your project to ESM
Things are a litle different for users with configuration files authored in TypeScript, and we’ll get to that.
Keep your config file the same and use the .cjs
extension
This is the simplest possible path forward. Update bud.config.js
to bud.config.cjs
.
Caveat: you cannot require
code from @roots/bud
or any first-party extension. Mostly bud.js is structured so
this isn’t a normal use case, but if you are using a require statement you’ll want to read up on
how to import esm from commonjs.
Update your config file to use ESM syntax and use the extension .mjs
.
This is probably my recommended approach. Update bud.config.js
to bud.config.mjs
.
Update your config function to use the ESM export
syntax:
module.exports = async bud => bud.entry('app', 'index')
becomes:
export default async bud => bud.entry('app', 'index')
Transition your project to ESM
This is the most involved approach. There is no way I can cover it fully in this release post. But, I will try to sketch it out:
First, add a type
field to package.json
indicating your project is opting in to ESM:
{
"name": "project",
"type": "module"
}
Then update bud.config.js
to use export
syntax as described above.
You will also need to update all other files in your project accordingly. Some build tools allow for using export
syntax. Others do not.
For example, stylelint
does not support export
. So you will update stylelint.config.js
to stylelint.config.cjs
.
Jest, on the other hand, does support export
. You will either want to update your jest config file to use ESM export
syntax or rename it to jest.config.cjs
.
Check the documentation for each tool. Read up on ESM.
TypeScript guide
Update config to either bud.config.mts
or bud.config.cts
TypeScript 4.7.2 offers two new TypeScript extensions to deal with CJS/ESM compatibility issues: .cts
and .mts
(they have their own declaration file extensions as well: .d.cts
and .d.mts
).
In general, I’d imagine most TS users already author their bud.config.ts
file with export
syntax. If that’s the case, you will likely just want to update the file name to bud.config.mts
.
Use ts-bud
instead of bud
Due to the way ESM modules are loaded you’ll need to use ts-bud
instead of bud
when running cli commands. Explanation follows:
A problem with TypeScript and ESM: it is not possible to hack import
at runtime the way we can hack require
at runtime.
bud.js uses ts-node to import TS configs when it is available, but with ESM we also need to register an import
loader so that the config file can be parsed. This can’t be done at runtime.
ts-node offers a flag to set this up:
ts-node --esm --transpileOnly
And bud.js offers a bin
that wraps the standard bud
command accordingly: ts-bud
. Use it instead of bud
.
If this doesn’t work for you, or you need to adjust other ts-node
flags, you may do this yourself:
ts-node --esm --transpileOnly ./node_modules/.bin/bud build
Known issue: bud.typescript.typecheck.enable()
will die when using ts-bud
It is unclear what the problem is as of right now (see #1480). In order to enable typechecking you must author your config file in JS until this is resolved.
Notes on import vs. require in the context of bud.js
For the most part this shouldn’t be an issue. It isn’t typical to import or require bud.js code from a project config.
There are exceptions however. For example, if you are using the bud.js node api to generate a config for use with the webpack-cli
.
Updating require
statements to import
If you are writing a config file with .mjs
or you have opted in with "type": "module"
, you will no longer
be able to require
modules or packages in your config file.
The great news is that it’s totally possible import CommonJS from an ES Module, so you can convert require
statements to use import
without worrying about it too much:
const value = require('browsersync-webpack-plugin')
becomes:
import value from 'browsersync-webpack-plugin'
Importing ESM from CommonJS
:::danger
You absolutely cannot require
bud.js core. No CommonJS exports are offered.
:::
Importing ESM from CommonJS is a little less straight forward than the other way around. This is one of the reasons we recommend converting your config to .mjs
.
The easiest way is to use a dynamic import
statement. The biggest difference is that a dynamic import is asynchronous, whereas require is sync.
export default async bud => {
await import('browsersync-webpack-plugin')
}
In these cases you may find that the code returned from a dynamic import
is set inside of a property called default
.
Using the bud.module
helper utility
If you wish you may use bud.module.import
instead of import
(which will automatically return the value of default
, if it is set):
export default async bud => {
await bud.module.import('browsersync-webpack-plugin')
}
The node:module
interface provides a function createRequire
that will let you directly require CommonJS
code like you may be used to. See the nodejs docs on createRequire
.
bud.js has an instance of the Require
function available at bud.module.require
(it’s context is the directory containing the project package.json
):
export default async bud => {
bud.module.require('browser-sync-webpack-plugin')
// require.resolve works too
bud.module.require.resolve('browser-sync-webpack-plugin')
}
If you just want the path to a module and aren’t sure if it is CommonJS or ESM, you may use bud.module.resolve
:
export default async bud => {
await bud.module.resolve('browser-sync-webpack-plugin')
}
This functionality is provided using the import-meta-resolve
package. In the future we’ll use the import.meta.resolve
API directly but right now it is labeled as experimental
and requires a flag.
Additional details
Features
- New utility package:
@roots/wordpress-hmr
. Greatly simplifies block editor development for WordPress friends. I’m not sure where this fits into the docs yet, but there is usage information available on this page for now. - You can now use remote modules as if they were local. Check the documentation on remote sources for more information. This works, but it should be considered experimental. Let us know how it goes.
- You can now transpile to
esm
. This feature is not yet documented and should be considered experimental. You can try it out withbud.esm.enable()
, but roots/sage users in particular should be aware that this will
require additional setup. Acorn support for this feature is being worked out. - Critical CSS extension got an update and better documentation to go along with it.
Fixes
- The error overlay got a little janked in 5.8. It’s fixed now.
Notes
- All extensions now provide a predictable export of
./extension
(eg:@roots/bud-react/extension
). - All
stylelint
andeslint
presets are exported with the.cjs
extension. If your eslint or stylelint config is already using the preset without an extension (as documented) you don’t have to do anything. If you are specifying.js
you will need to update it. - The
lib
andtypes
directories for all packages have been merged. - All published code targets
es2021
. Update to node 16 if you are running an outdated version of node. -
@roots/bud-support
is deprecated -
@roots/bud-library
is deprecated (the default caching configuration is now better than this extension)
For more information review the diff to see what’s changed.