Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A config for esbuild #8

Closed
afomera opened this issue Sep 7, 2021 · 15 comments
Closed

A config for esbuild #8

afomera opened this issue Sep 7, 2021 · 15 comments

Comments

@afomera
Copy link

afomera commented Sep 7, 2021

👋

After using esbuild for a few days via the esbuild-rails gem and now this gem, one thing I've found myself needing is to make an esbuild.config.js file across all of my applications.

I realize from the other gem issues one concern is that it'd turn into a mini webpacker, but config files are fairly common across JS packages and even the two other bundlers in this gem have config files so I found it super odd that esbuild does not have one generating out of the box.

Additionally, it helps ease the burden of where do I add a new config/plugin when I want to use it with esbuild, the end user then has to go create a config file and update their build script to use it, which doesn't feel quite well.

I've been enjoying having my esbuild.config.js file in the root level (Same as gemfile / rollup.config.js etc etc). I know in rails/esbuild-rails#2 you mentioned it may be better to do documentation with a link to learning where to make a config file, but that feels like a very poor developer UX when its fairly straightforward to support this basic of a config file... and it basically matches the level of configuration the rollup.config.js is.

This is of course basically what @jasoncharnes had in esbuild-rails#2. You could drop the watch function / arg down in the build but you loose the output and honestly having the build finished output is quite nice to know it... well finished.

const watch = process.argv.includes("--watch") && {
  onRebuild(error) {
    if (error) console.error("[watch] build failed", error);
    else console.log("[watch] build finished");
  },
};

 require("esbuild").build({
  entryPoints: ["app/javascript/application.js"],
  bundle: true,
  outdir: "app/assets/builds",
  watch: watch,
  plugins: [],
}).catch(() => process.exit(1));

From this point a developer can customize their esbuild system how they'd like, adding plugins to load whatever they need and so forth. It was a fair bit to explain how to make this file in a screencast I recorded, and feels like a point where someone will easily get stuck.

I hope you'll reconsider!

@dhh
Copy link
Member

dhh commented Sep 7, 2021

Happy to leave the discussion open for now. I think the part that feels different to me is that esbuild's config is more like an actual program that uses esbuild as an API. Perhaps that's a subtle difference, but it feels distinct to me. When you literally have to parse your own ARGV, then it's clearly a different territory to rollup.config.js or webpack.config.js.

@t27duck
Copy link
Contributor

t27duck commented Sep 7, 2021

Does --watch work with esbuild AND a config file?

If so, a possible compromise could be a file with only

 require("esbuild").build({
  entryPoints: ["app/javascript/application.js"],
  bundle: true,
  outdir: "app/assets/builds",
  plugins: [],
}).catch(() => process.exit(1));

This way, you have a clear place to know that (1) plugins exist, (2) where to put them, (3) a place to put other configuration for esbuild similar to rollup and webpack, and (4) for those who are "in the know" about configuring the watch option through the esbuild api with callbacks could then add it in.

@dhh
Copy link
Member

dhh commented Sep 10, 2021

@t27duck It does not. You have to implement it yourself, like afomera did.

@xiaohui-zhangxh
Copy link

@t27duck It does not. You have to implement it yourself, like afomera did.

what about write two files: esbuild.config.js and esbuild.node.js, although a better solution should be making a PR to esbuild that support passing config file to esbuild-cli.

// esbuild.config.js

const config = {
  entryPoints: ["app/javascript/application.js"],
  bundle: true,
  outdir: "app/assets/builds",
  plugins: [],
}

export const config
// esbuild.node.js

import { config } from './esbuild.config.js'

// merge default plugins just like glob import for stimulusjs autoloader

require("esbuild").build(config).catch(() => process.exit(1));

@dhh
Copy link
Member

dhh commented Nov 5, 2021

Yeah, I'd like to use esbuild out of the box. We do support other bundlers. If esbuild decides to implement a way to pass in a config, great, we can use that. Otherwise, it'll be an exercise for the reader.

@dhh dhh closed this as completed Nov 5, 2021
@krokodaxl
Copy link

We were facing a similar question and wanted to use the built in minifcation of esbuild. So we tried to make good use of the fact you can use the arguments yarn build is called with in the config file.
jsbundling-rails does 2 things:

  • assist in the installation and setup of a bundler
  • enhance rake tasks

If you write the rake tasks by yourself the gem can be removed easily after the installation and setup phase.

esbuild.config.js looks like this:

const path = require('path')
const svgrPlugin = require('esbuild-plugin-svgr');

require("esbuild").build({
  entryPoints: ["application.js"],
  bundle: true,
  outdir: path.join(process.cwd(), "app/assets/builds"),
  absWorkingDir: path.join(process.cwd(), "app/javascript"),
  watch: process.argv.includes("--watch"),
  plugins: [svgrPlugin()],
  minify: process.argv.includes("--minify")
}).catch(() => process.exit(1))

changed package.json to use

    "scripts": {
        "build": "node esbuild.config.js"
    }

Procfile.dev

web: bin/rails server
js: yarn build --watch

last we added build.rake under lib/tasks to add minify as command argument in production.

namespace :javascript do
  desc 'Build your JavaScript bundle'
  task :build do
    command = 'yarn install && yarn build'
    command += ' --minify' if Rails.env.production?

    raise 'Ensure yarn is installed and `yarn build` runs without errors' unless system command
  end
end

Rake::Task["assets:precompile"].enhance(["javascript:build"]) 

Did the trick for us to watch in development and minify in production with one esbuild.config.js

@nkokkos
Copy link

nkokkos commented Jan 25, 2023

Hi, it seems that @krokodaxl 's config works great with esbuild 0.16.x.

But if you upgrade to esbuild 0.17.x, compile fails because "watch" option is removed.

See here: https://github.com/evanw/esbuild/blob/main/CHANGELOG.md#0170

"...This change removes the incremental and watch options as well as the serve() method, and introduces a new context() method..."

@salimhb
Copy link

salimhb commented Jan 25, 2023

I migrated my code to this:

const path = require('path')

require("esbuild").context({
  entryPoints: ["application.js"],
  bundle: true,
  sourcemap: true,
  publicPath: 'assets',
  outdir: path.join(process.cwd(), "app/assets/builds"),
  absWorkingDir: path.join(process.cwd(), "app/javascript"),
  plugins: [],
  minify: process.argv.includes("--minify")
}).then(context => {
  if (process.argv.includes("--watch")) {
    // Enable watch mode
    context.watch()
  } else {
    // Build once and exit if not in watch mode
    context.rebuild().then(result => {
      context.dispose()
    })
  }
}).catch(() => process.exit(1))

@spaquet
Copy link

spaquet commented Jan 28, 2023

@salimhb Thx!

@maricavor
Copy link

Thnx!

@interphased
Copy link

@salimhb This config works great, but I'm having issues with esbuild not watching newly created JS files. It seems to only watch the files that existed when it was first run. To pick up the new files esbuild --watch needs to be stopped and re-run. I'm wondering if you have this issue too, or if it's unrelated and maybe to do with my Docker setup or something else in my project. It's just weird because SASS and PostCSS (using cssbundling-rails) have no problem picking up newly created files, leading me to think it's an esbuild/config issue.

@salimhb
Copy link

salimhb commented May 3, 2023

@interphased I'm not having an issue with watch mode. I'm not using docker. I'm running node locally using yarn build --watch and package.json has:

  "scripts": {
    "build": "node config/esbuild.config.js",
    "build:css": "sass ./app/assets/stylesheets/application.sass:./app/assets/builds/application.css --no-source-map --load-path=node_modules"
  },

esbuild 0.17.17 has a fix for an edge case, where watch doesn't work. I don't know if this is the problem you are facing. Perhaps also upgrade to the latest version.

I hope this helps.

@interphased
Copy link

interphased commented May 3, 2023

@salimhb Thanks for checking and providing a possible solution. I tried updating esbuild to no avail, so I'm still not sure why it's not watching new files correctly. It seems isolated to esbuild so I may open a ticket there at some point.

In the end, I implemented chokidar to watch for changes instead, and now it works. ¯\_(ツ)_/¯

Example:

if (process.argv.includes('--watch')) {
  (async () => {
    chokidar.watch('my-watch-directory').on('all', async (_, p) => {
      console.log(`Changes were detected in: ${p}`);
      console.log('Rebuilding...');
      await ctx.rebuild();
    });
  })();
} else {
  await ctx.rebuild().then(() => {
    ctx.dispose();
  });
}

@jroes
Copy link

jroes commented Feb 21, 2024

How are folks handling static files that just need to get copied over? For example, I'm midway through a move from webpacker to webpack to esbuild (had to go step-by-step to try and stay sane), and have this:

const esbuild = require("esbuild");
import { sassPlugin } from "esbuild-sass-plugin";
import postcss from "postcss";
import autoprefixer from "autoprefixer";
const postcssPresetEnv = require("postcss-preset-env");
import tailwindcss from "tailwindcss";
const tailwindConfig = require("./tailwind.config.js");

const watch = process.argv.includes("--watch");

const args = {
  entryPoints: [
    //    "./app/webpacker/packs/application.js",
    "./app/webpacker/packs/stylesheets.scss",
    //    "./app/webpacker/packs/admin.js",
    "./app/webpacker/packs/admin.scss",
  ],
  outdir: "./app/assets/builds",
  bundle: true,
  metafile: true,
  loader: {
    ".js": "jsx",
    ".png": "file",
    ".woff": "file",
    ".woff2": "file",
    ".eot": "file",
    ".ttf": "file",
    ".svg": "file",
    ".gif": "file",
    ".jpg": "file",
  },
  plugins: [
    sassPlugin({
      async transform(source, resolveDir) {
        const { css } = await postcss([tailwindcss(tailwindConfig), autoprefixer, postcssPresetEnv]).process(source, {
          from: undefined,
        });
        return css;
      },
    }),
  ],
};

let ctx;
if (watch) {
  args.minify = false;
  ctx = await esbuild.context(args);
  await ctx.watch();
  console.log("watching...");
} else {
  args.minify = true;
  ctx = await esbuild.build(args);
  console.log("build successful");
}

And one of the (several) issues I'm facing is porting over asset_pack_path calls. I believe the static files are getting copied into app/assets/builds, but how to reference those with the asset_path helper is a little unclear to me (doesn't work 1:1, because the paths aren't getting preserved from what I can tell).

Apologies if this is not a great forum for this question, but finding good sources of info on these topics is fairly challenging. Hope this adds a data point to the kinds of struggles folks are experiencing trying to get migrated to the latest practices.

@triskweline
Copy link

How are folks handling static files that just need to get copied over?

We are doing it like this:

  • Process static files with esbuild, like in the config you posted.
  • Use esbuild-manifest-plugin to have esbuild generate a manifest.json file mapping logical paths (example.png) to fingerprinted paths (example-5N2N2WJM.png).
  • Use the precompiled_assets gem to Rails helpers like image_path or javascript_include_tag resolve paths using the generated manifest.json.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests