Roots Discourse

Bud and ECMAScript Modules (ESM)

Hi,

I try to include the package grrr-amsterdam/cookie-consent with Bud. They have a page how to use it with build tools. With Laravel Mix I included the config below:

mix
  .webpackConfig({
    module: {
      rules: [
        {
          test: /\.mjs$/i,
          resolve: {byDependency: {esm: {fullySpecified: false}}},
        },
      ],
    }
  });

I don’t know how to do this with Bud and it gives this error:

Module not found: Error: Can't resolve './src/cookie-consent' in '/Users/[..]/site/web/app/themes/sage/node_modules/@grrr/cookie-consent'
Did you mean 'cookie-consent.mjs'?
BREAKING CHANGE: The request './src/cookie-consent' failed to resolve only because it was resolved as fully specified
(probably because the origin is strict EcmaScript Module, e. g. a module with javascript mimetype, a '*.mjs' file, or a '*.js' file where the package.json contains '"type": "module"').
The extension in the request is mandatory for it to be fully specified.
Add the extension to the request.

Any hints how to get this working with the Bud config?

2 Likes

Bud has a similar method. You could maybe try this. Let us know if this works.

.override(webpackConfig => {
  webpackConfig.module.rules.push({
    test: /\.mjs$/i,
    resolve: {byDependency: {esm: {fullySpecified: false}}},
  })
})

@kellymears might have a better solution.

1 Like

bud looks like laravel-mix but more roots-/sage-specific?

@kellymears compares Bud vs Mix vs Encore in the linked issue below.

2 Likes

Thanks for helping out. Unfortunately I can’t get it working. It still shows the same error. I use the current master branch of Sage and changed the bud.config.js to config seen below. Is this the way you meant it?

/**
 * @typedef {import('@roots/bud').Bud} Bud
 *
 * @param {Bud} config
 */

module.exports = async (config) =>
  config
    /**
     * Application entrypoints
     *
     * Paths are relative to your resources directory
     */
    .entry({
      app: ['scripts/app.js', 'styles/app.css'],
      editor: ['scripts/editor.js', 'styles/editor.css'],
      customizer: 'scripts/customizer.js',
    })

    .override(webpackConfig => {
      webpackConfig.module.rules.push({
        test: /\.mjs$/i,
        resolve: {byDependency: {esm: {fullySpecified: false}}},
      })
    })

    /**
     * These files should be processed as part of the build
     * even if they are not explicitly imported in application assets.
     */
    .assets(['assets/images'])

    /**
     * These files will trigger a full page reload
     * when modified.
     */
    .watch([
      'tailwind.config.js',
      'resources/views/*.blade.php',
      'app/View/**/*.php',
    ])

    /**
     * Target URL to be proxied by the dev server.
     *
     * This is your local dev server.
     */
    .proxy({target: 'http://example.test'});

Edit: Actually, let’s wait for @kellymears to weigh in, because I’m not sure if build.rules.mjs is correct, or if it should be something else, or if more steps are necessary.

Can you try adding this as well?

    .tap(({build}) => {
      build.rules.mjs.setExclude(() => /node_modules[\/\\](?!.*\.mjs$)/);
    })

You might need to adjust that regex; I’m basically trying to make it exclude everything in node_modules, except .mjs files.

I’m realizing now, of course, that my implementation of mix’s override utility is somewhat flawed. i have made an issue to track improving the utility to match your expectations.

In the meantime… there are a few ways to handle this today.

For readability, i’m going to refer to mjs in the following examples, which is this object:

const mjs = {
  test: /\.mjs$/i,
  resolve: {byDependency: {esm: {fullySpecified: false}}},
}

using the config.override hook

bud.hooks.on('config.override', (configs) => {
  configs[0].module.rules.unshift(mjs);
  return configs;
})

Note that i am specifying configs[0].

Bud always passes the final configuration to webpack as an array – even if there is only one compiler being used. this is because webpack provides different values for stats and the like depending on if you give it a single config or an array of configs (multi-compiler mode). it simplifies things greatly to always get back the same type of response from webpack so we just always pass an array.

using one of the build.module.rules hooks

There are actually three sets of module.rules for bud. I’m not sure which is best for your project but all of them will be pretty much identical in terms of performance unless you are doing something really complex.

This is the shape of what i’m about to describe, for context:

module: {
  rules: [
    ...before,  // build.module.rules.before
    oneof,      // build.module.rules.oneOf
    ...after,   // build.module.rules.after
  ],
}

build.module.rules.oneOf

This is the standard type of rule for bud. A oneOf rule in webpack is a nested rule which is no longer processed after the first match. This makes it much more performant than passing an object to every rule. Transpiling JS is slow enough as-is :stuck_out_tongue:

To modify the oneOf ruleset you can use the build.module.rules.oneOf hook:

hooks.on('build.module.rules.oneOf', (rules) => [...rules, mjs]);

build.module.rules.before

This is a top-level rule which is added to the array before oneOf. Bud uses this ruleset to help ensure that no node stuff makes it into the browser. @roots/bud-vue uses it for vue-loader because Vue is incompatible with oneOf (for some reason).

hooks.on('build.module.rules.before', (rules) => [...rules, mjs]);

build.module.rules.after

This is a top-level rule which is added to the array after oneOf. Nothing uses this hook in bud or any of its extensions, but it kind of feel like the sort of thing that should be there in case someone needs it.

hooks.on('build.module.rules.after', (rules) => [...rules, mjs]);

Examples

Check out this gist I made for an example of each of these hooks in the context of Sage. I would start by trying to use a oneOf rule but they all should work.

3 Likes

I now have a draft PR proposing the change to bud.override.

It also adds a new hook that is simpler than the config.override hook for projects that do not use more than one bud instance – if anybody wants to use the filter api directly.

bud.hooks.on('build.override', config => config)
2 Likes

Thank you for your extensive explanation! It’s all clear to me now and I got everything working with your examples! Bud looks very promising :smiley:

1 Like