Skip to main content

Release: 6.0.0

· 8 min read
Kelly Mears

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.

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:

  1. Keep your config file the same and use the .cjs extension
  2. Update your config file to use ESM syntax and use the .mjs extension
  3. 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:

bud.config.js
module.exports = async bud => bud.entry('app', 'index')

becomes:

bud.config.mjs
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:

package.json
{
"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

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.

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:

shell
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:

shell
ts-node --esm --transpileOnly ./node_modules/.bin/bud build

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:

bud.config.cjs
const value = require('browsersync-webpack-plugin')

becomes:

bud.config.mjs
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.

bud.config.cjs
module.exports = 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):

bud.config.cjs
module.exports = 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 module 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):

bud.config.mjs
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:

bud.config.mjs
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 with bud.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 and eslint 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 and types 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.