Skip to content

Instantly share code, notes, and snippets.

@KonnorRogers
Last active October 3, 2022 17:25
Show Gist options
  • Star 31 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save KonnorRogers/54759bc8553a22d507fbc97e4eae14cc to your computer and use it in GitHub Desktop.
Save KonnorRogers/54759bc8553a22d507fbc97e4eae14cc to your computer and use it in GitHub Desktop.
ESBuild with Webpacker < 6 in Rails. Bye Babel <3
// DONT FORGET TO `yarn add esbuild-loader` !!!
// config/webpacker/environment.js
const { environment } = require('@rails/webpacker')
const { ESBuildPlugin } = require('esbuild-loader')
const esBuildUse = [
{
loader: require.resolve('esbuild-loader'),
// What you want to compile to, in this case, ES7
options: { target: 'es2016' }
}
]
environment.loaders.get('babel').use = esBuildUse
environment.loaders.get('nodeModules').use = esBuildUse
environment.plugins.append("EsBuildPlugin", new ESBuildPlugin())
module.exports = environment

This gist will modify your babel-loader in place to use esbuild-loader instead. Also by default webpacker 5 and below compile your node_modules, so it also modifies the loader for that as well.

Esbuild is a super fast javascript transpiler built in Go. It also uses nice language like: targets: es7 so no more fighting with .browserslistrc

It will also auto transform syntax like: static values = {}

Heres a full list of transforms: https://esbuild.github.io/content-types/#javascript

Esbuild Loader: https://github.com/privatenumber/esbuild-loader

Esbuild: https://esbuild.github.io/

Getting started

yarn install esbuild-loader

Then modify your config/webpacker/environment.js to look as above.

Thats it! You can now delete your babel.config.js and your .browserslistrc and you can simply target an es{x} version!

@KonnorRogers
Copy link
Author

KonnorRogers commented Feb 6, 2021

This is not currently working right now, something under the hood with webpacker is still expecting babel....interesting.

@KonnorRogers
Copy link
Author

Found the issue. Im using Webpacker 6 which no longer uses environment and instead uses rules !

@KonnorRogers
Copy link
Author

Confirmed working without babel on 5.2.1

@geoffharcourt
Copy link

geoffharcourt commented Mar 22, 2021

@ParamagicDev thanks for this awesome example! have you worked out how to do this with the newest release of esbuild-loader which drops the ESBuildPlugin? I think it's just dropping the two plugin-related lines.

@KonnorRogers
Copy link
Author

Didnt realize they dropped the plugin! Perhaps the following could work??

const { environment } = require('@rails/webpacker')

const esBuildUse = [
  {
    loader: require.resolve('esbuild-loader'),
    // What you want to compile to, in this case, ES7
    options: { target: 'es2016' }
  }
]

environment.loaders.get('babel').use = esBuildUse
environment.loaders.get('nodeModules').use = esBuildUse

module.exports = environment

@geoffharcourt
Copy link

Yeah, as soon as I asked I realized it was working. Got too aggressive and deleted the esBuildUse statements too.

Thanks again for this snippet, it was a huge help to us.

@KonnorRogers
Copy link
Author

@geoffharcourt Its my current compromise short of swapping to https://github.com/ElMassimo/vite_ruby ! I hate the .browserslist syntax with a passion that burns inside of me. I also genuinely dont care for Babel. Glad it helped you! I was so frustrated trying to setup web components in a rails projects I just threw my arms up in the air and looked to see if Webpack supported ESbuild.

I need to work on a port for Webpacker 6, but its still in beta so ill wait until everything is finalized.

All that to say, glad I could help!

@geoffharcourt
Copy link

We are working on a Rails integration with esbuild ATM, I'll post a link here when we're ready to share it.

@KonnorRogers
Copy link
Author

Sounds good to me! Cant wait to see it!

@soulcurrymedia
Copy link

@geoffharcourt any updates on Rails integration with esbuild?

@geoffharcourt
Copy link

geoffharcourt commented Jun 20, 2021

We ended up building something that's so simple that it's not even very esbuild-specific. We write a Webpack-like manifest with this esbuild plugin:

const path = require('path');
const fs = require('fs');

const elapsedTime = (startTime) => {
  const timeDiff = new Date().getTime() - startTime.getTime();

  return `, took ${Math.round(timeDiff / 1000)} seconds`;
};

const writeManifest = (result, build) => {
  console.log('\x1b[33m%s\x1b[0m', 'Writing metafile...');

  const manifestData = Object.keys(result.metafile.outputs)
    .reduce(
      (accumulator, outputName) => {
        const keyFullName = outputName.replace(`${build.initialOptions.outdir}/`, '');
        const keyNameSegments = keyFullName.split('.');
        keyNameSegments[keyNameSegments.length - 2] = keyNameSegments[keyNameSegments.length - 2]
          .split('-')
          .slice(0, -1)
          .join('-');
        const nameWithoutFingerprint = keyNameSegments.join('.');

        accumulator[nameWithoutFingerprint] = outputName.replace(
          build.initialOptions.outdir,
          build.initialOptions.publicPath,
        );

        return accumulator;
      },
      {},
    );

  fs.writeFileSync(
    path.join(build.initialOptions.outdir, 'manifest.json'),
    JSON.stringify(manifestData),
  );

  console.log('\x1b[32m%s\x1b[0m', 'Updated manifest');
};

const manifestBuilderPlugin = {
  name: 'manifestBuilder',
  setup(build) {
    let startTime = new Date();

    build.onStart(() => {
      startTime = new Date();
    });

    build.onEnd((result) => {
      if (result.errors.length === 0) {
        writeManifest(result, build);
        console.log('\x1b[34m%s\x1b[0m', `Built${elapsedTime(startTime)}`);
      }
    });
  },
};

module.exports = manifestBuilderPlugin;

Then we replace Webpacker's manifest mapping:

module Esbuild
  extend self

  def instance
    @instance ||= Instance.new
  end

  class Instance
    def initialize(manifest_location = Rails.root.join("public/builds/manifest.json"))
      @manifest_location = manifest_location
    end

    def manifest
      if Rails.env.development?
        load_manifest_file
      else
        @manifest ||= load_manifest_file
      end
    end

    private

    attr_reader :manifest_location

    def load_manifest_file
      JSON.parse(File.read(manifest_location))
    rescue Errno::ENOENT
      raise ManifestFileNotFound.new(<<-MSG.squish)
        Unable to find public/builds/manifest.json.
        You may need to re-run esbuild and the manifest writer.
      MSG
    rescue JSON::ParserError
      raise ManifestNotValid
    end
  end

  class ManifestFileNotFound < StandardError; end

  class ManifestNotValid < StandardError; end
end


module EsbuildHelper
  def javascript_esbuild_tag(base_name, css: true, defer: true)
    js_path = current_esbuild_manifest.fetch("#{base_name}.js")

    tags = javascript_include_tag(js_path, media: :all, defer: defer)

    if css
      css_path = current_esbuild_manifest.fetch("#{base_name}.css", nil)

      if css_path
        tags << " "
        tags << stylesheet_link_tag(css_path, media: :all)
      end
    end

    tags
  rescue KeyError
    raise AssetNotFoundInManifest.new(base_name)
  end

  def stylesheet_esbuild_tag(base_name)
    css_path = current_esbuild_manifest.fetch("#{base_name}.css")

    stylesheet_link_tag(css_path, media: :all)
  rescue KeyError
    raise AssetNotFoundInManifest.new(base_name)
  end

  def preload_esbuild_asset_tag(asset_name, **options)
    preload_link_tag(current_esbuild_manifest[asset_name], options)
  end

  def esbuild_asset_path(asset_name)
    current_esbuild_manifest.fetch(asset_name)
  rescue KeyError
    raise AssetNotFoundInManifest.new(asset_name)
  end

  def current_esbuild_manifest
    Esbuild.instance.manifest
  end

  class AssetNotFoundInManifest < StandardError; end
end

I think you could probably just use the manifest-writing plugin and continue to use Webpacker's asset pack path helpers if you wanted to minimize bespoke code.

This change turned our 8-18 minute deploys to < 3m, and we haven't had a single issue related to esbuild and asset mapping. I'm not ready to support it for others but I'm happy to share what we've used.

@soulcurrymedia
Copy link

@geoffharcourt thank you very much for sharing, with us.

@ndrean
Copy link

ndrean commented Aug 28, 2021

How should I use ESBuild and your snippet?I currently use Webapcker 6 with:

//   /config/webpack/base.js
const { webpackConfig, merge } = require('@rails/webpacker')
const customConfig = {
  resolve: {
    alias: {
      react: "preact/compat",
      "react-dom": "preact/compat",
    },
    extensions: ["css"],
  },
  // plugins: [
  //   new BundleAnalyzerPlugin(),
};
module.exports = merge(webpackConfig, customConfig)

and

// /config/webpack/prod-dev.js
const webpackConfig = require('./base')

const { environment } = require("@rails/webpacker");
console.log(environment);   <--- "undefined"

module.exports = webpackConfig

I don't see where the module environment is used so webpack still compiles. I hope this question makes sense.

The keys I see in @rails/webpacker are [config, devServer, webpackConfig, baseConfig, rules, merge, moduleExists...] but not `environment.

./node_modules/.bin/esbuild --version => 0.12.2

@geoffharcourt
Copy link

geoffharcourt commented Aug 28, 2021

@ndrean you don't use Webpacker at all with this solution, this is a (small) replacement for it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment