Skip to content

Commit e308e76

Browse files
authored
Add Bun support (#167)
* Add Bun support * Tweak README * Tweak README for clarity
1 parent a6964f3 commit e308e76

File tree

8 files changed

+118
-26
lines changed

8 files changed

+118
-26
lines changed

README.md

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,41 @@
11
# JavaScript Bundling for Rails
22

3-
Use [esbuild](https://esbuild.github.io), [rollup.js](https://rollupjs.org), or [Webpack](https://webpack.js.org) to bundle your JavaScript, then deliver it via the asset pipeline in Rails. This gem provides installers to get you going with the bundler of your choice in a new Rails application, and a convention to use `app/assets/builds` to hold your bundled output as artifacts that are not checked into source control (the installer adds this directory to `.gitignore` by default).
3+
Use [Bun](https://bun.sh), [esbuild](https://esbuild.github.io), [rollup.js](https://rollupjs.org), or [Webpack](https://webpack.js.org) to bundle your JavaScript, then deliver it via the asset pipeline in Rails. This gem provides installers to get you going with the bundler of your choice in a new Rails application, and a convention to use `app/assets/builds` to hold your bundled output as artifacts that are not checked into source control (the installer adds this directory to `.gitignore` by default).
44

55
You develop using this approach by running the bundler in watch mode in a terminal with `yarn build --watch` (and your Rails server in another, if you're not using something like [puma-dev](https://github.com/puma/puma-dev)). You can also use `./bin/dev`, which will start both the Rails server and the JS build watcher (along with a CSS build watcher, if you're also using `cssbundling-rails`).
66

77
Whenever the bundler detects changes to any of the JavaScript files in your project, it'll bundle `app/javascript/application.js` into `app/assets/builds/application.js` (and all other entry points configured). You can refer to the build output in your layout using the standard asset pipeline approach with `<%= javascript_include_tag "application", defer: true %>`.
88

9-
When you deploy your application to production, the `javascript:build` task attaches to the `assets:precompile` task to ensure that all your package dependencies from `package.json` have been installed via yarn, and then runs `yarn build` to process all the entry points, as it would in development. The latter files are then picked up by the asset pipeline, digested, and copied into public/assets, as any other asset pipeline file.
9+
When you deploy your application to production, the `javascript:build` task attaches to the `assets:precompile` task to ensure that all your package dependencies from `package.json` have been installed via your javascript package manager (bun or yarn), and then runs the build script defined in `package.json` to process all the entry points, as it would in development. The latter files are then picked up by the asset pipeline, digested, and copied into public/assets, as any other asset pipeline file.
1010

1111
This also happens in testing where the bundler attaches to the `test:prepare` task to ensure the JavaScript has been bundled before testing commences. If your testing library of choice does not call the `test:prepare` Rake task, ensure that your test suite runs `javascript:build` to bundle JavaScript before testing commences.
1212

1313
That's it!
1414

15-
You can configure your bundler options in the `build` script in `package.json` or via the installer-generated `rollup.config.js` for rollup.js or `webpack.config.json` for Webpack (esbuild does not have a default configuration format, and we don't intend to use esbuild as an API in order to hack around it).
15+
You can configure your bundler options in the `build` script in `package.json` or via the installer-generated `bun.config.js` for Bun, `rollup.config.js` for rollup.js or `webpack.config.json` for Webpack (esbuild does not have a default configuration format, and we don't intend to use esbuild as an API in order to hack around it).
1616

1717
If you're already using [`webpacker`](https://github.com/rails/webpacker) and you're wondering if you should migrate to `jsbundling-rails`, have a look at [the high-level comparison](./docs/comparison_with_webpacker.md). If you're looking to migrate from webpacker, see the [migration guide](https://github.com/rails/jsbundling-rails/blob/main/docs/switch_from_webpacker.md).
1818

1919
If you want to use webpack features like [code splitting](https://webpack.js.org/guides/code-splitting/) and [hot module reloading](https://webpack.js.org/concepts/hot-module-replacement/), consider using the official fork of `webpacker`, [`shakapacker`](https://github.com/shakacode/shakapacker).
2020

2121
## Installation
22+
If you are installing esbuild, rollup, or webpack, you must already have node
23+
and yarn installed on your system. You will also need npx version 7.1.0 or later.
2224

23-
You must already have node and yarn installed on your system. You will also need npx version 7.1.0 or later. Then run:
25+
If you are using Bun, then you must have the Bun runtime already installed on
26+
your system.
27+
28+
To get started run:
2429

2530
```
2631
./bin/bundle add jsbundling-rails
2732
```
2833

2934
```
30-
./bin/rails javascript:install:[esbuild|rollup|webpack]
35+
./bin/rails javascript:install:[bun|esbuild|rollup|webpack]
3136
```
3237

33-
Or, in Rails 7+, you can preconfigure your new application to use a specific bundler with `rails new myapp -j [esbuild|rollup|webpack]`.
38+
Or, in Rails 7+, you can preconfigure your new application to use a specific bundler with `rails new myapp -j [bun|esbuild|rollup|webpack]`.
3439

3540

3641
## FAQ
@@ -39,9 +44,9 @@ Or, in Rails 7+, you can preconfigure your new application to use a specific bun
3944

4045
The default build script for esbuild relies on the `app/javascript/*.*` glob pattern to compile multiple entrypoints automatically. This glob pattern is not available by default on Windows, so you need to change the build script in `package.json` to manually list the entrypoints you wish to compile.
4146

42-
### Why does esbuild overwrite my application.css?
47+
### Why does bun/esbuild overwrite my application.css?
4348

44-
If you [import CSS](https://esbuild.github.io/content-types/#css-from-js) in your application.js while using esbuild, you'll be creating both an `app/assets/builds/application.js` _and_ `app/assets/builds/application.css` file when bundling. The latter can conflict with the `app/assets/builds/application.css` produced by [cssbundling-rails](https://github.com/rails/cssbundling-rails). The solution is to either change the output file for esbuild (and the references for that) or for cssbundling. Both are specified in `package.json`.
49+
If you [import CSS](https://esbuild.github.io/content-types/#css-from-js) in your application.js while using esbuild or Bun, you'll be creating both an `app/assets/builds/application.js` _and_ `app/assets/builds/application.css` file when bundling. The latter can conflict with the `app/assets/builds/application.css` produced by [cssbundling-rails](https://github.com/rails/cssbundling-rails). The solution is to either change the output file for bun/esbuild (and the references for that) or for cssbundling. Both are specified in `package.json`.
4550

4651
### How can I reference static assets in JavaScript code?
4752

lib/install/bun/Procfile.dev

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
web: env RUBY_DEBUG_OPEN=true bin/rails server
2+
js: bun run build --watch

lib/install/bun/bun.config.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import path from 'path';
2+
import fs from 'fs';
3+
4+
const config = {
5+
sourcemap: "external",
6+
entrypoints: ["app/javascript/application.js"],
7+
outdir: path.join(process.cwd(), "app/assets/builds"),
8+
};
9+
10+
const build = async (config) => {
11+
const result = await Bun.build(config);
12+
13+
if (!result.success) {
14+
if (process.argv.includes('--watch')) {
15+
console.error("Build failed");
16+
for (const message of result.logs) {
17+
console.error(message);
18+
}
19+
return;
20+
} else {
21+
throw new AggregateError(result.logs, "Build failed");
22+
}
23+
}
24+
};
25+
26+
(async () => {
27+
await build(config);
28+
29+
if (process.argv.includes('--watch')) {
30+
fs.watch(path.join(process.cwd(), "app/javascript"), { recursive: true }, (eventType, filename) => {
31+
console.log(`File changed: ${filename}. Rebuilding...`);
32+
build(config);
33+
});
34+
} else {
35+
process.exit(0);
36+
}
37+
})();

lib/install/bun/install.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
require 'json'
2+
3+
if Rails.root.join("Procfile.dev").exist?
4+
append_to_file "Procfile.dev", "js: bun build --watch\n"
5+
else
6+
say "Add default Procfile.dev"
7+
copy_file "#{__dir__}/Procfile.dev", "Procfile.dev"
8+
9+
say "Ensure foreman is installed"
10+
run "gem install foreman"
11+
end
12+
13+
say "Add default bun.config.js"
14+
copy_file "#{__dir__}/bun.config.js", "bun.config.js"
15+
16+
say "Add build script to package.json"
17+
package_json = JSON.parse(File.read("package.json"))
18+
package_json["scripts"] ||= {}
19+
package_json["scripts"]["build"] = "bun bun.config.js"
20+
File.write("package.json", JSON.pretty_generate(package_json))

lib/install/install.rb

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,6 @@
3131
copy_file "#{__dir__}/package.json", "package.json"
3232
end
3333

34-
if Rails.root.join("Procfile.dev").exist?
35-
append_to_file "Procfile.dev", "js: yarn build --watch\n"
36-
else
37-
say "Add default Procfile.dev"
38-
copy_file "#{__dir__}/Procfile.dev", "Procfile.dev"
39-
40-
say "Ensure foreman is installed"
41-
run "gem install foreman"
42-
end
43-
4434
say "Add bin/dev to start foreman"
4535
copy_file "#{__dir__}/dev", "bin/dev"
4636
chmod "bin/dev", 0755, verbose: false

lib/install/install_node.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
if Rails.root.join("Procfile.dev").exist?
2+
append_to_file "Procfile.dev", "js: yarn build --watch\n"
3+
else
4+
say "Add default Procfile.dev"
5+
copy_file "#{__dir__}/Procfile.dev", "Procfile.dev"
6+
7+
say "Ensure foreman is installed"
8+
run "gem install foreman"
9+
end

lib/tasks/jsbundling/build.rake

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,37 @@
11
namespace :javascript do
22
desc "Install JavaScript dependencies"
33
task :install do
4-
unless system "yarn install"
5-
raise "jsbundling-rails: Command install failed, ensure yarn is installed"
4+
command = install_command
5+
unless system(command)
6+
raise "jsbundling-rails: Command install failed, ensure #{command.split.first} is installed"
67
end
78
end
89

910
desc "Build your JavaScript bundle"
1011
build_task = task :build do
11-
unless system "yarn build"
12-
raise "jsbundling-rails: Command build failed, ensure `yarn build` runs without errors"
12+
command = build_command
13+
unless system(command)
14+
raise "jsbundling-rails: Command build failed, ensure `#{command}` runs without errors"
1315
end
1416
end
15-
build_task.prereqs << :install unless ENV["SKIP_YARN_INSTALL"]
17+
18+
build_task.prereqs << :install unless ENV["SKIP_YARN_INSTALL"] || ENV["SKIP_BUN_INSTALL"]
19+
end
20+
21+
def install_command
22+
return "bun install" if File.exist?('bun.lockb') || (tool_exists?('bun') && !File.exist?('yarn.lock'))
23+
return "yarn install" if File.exist?('yarn.lock') || tool_exists?('yarn')
24+
raise "jsbundling-rails: No suitable tool found for installing JavaScript dependencies"
25+
end
26+
27+
def build_command
28+
return "bun run build" if File.exist?('bun.lockb') || (tool_exists?('bun') && !File.exist?('yarn.lock'))
29+
return "yarn build" if File.exist?('yarn.lock') || tool_exists?('yarn')
30+
raise "jsbundling-rails: No suitable tool found for building JavaScript"
31+
end
32+
33+
def tool_exists?(tool)
34+
system "command -v #{tool} > /dev/null"
1635
end
1736

1837
unless ENV["SKIP_JS_BUILD"]

lib/tasks/jsbundling/install.rake

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,28 @@ namespace :javascript do
55
system "#{RbConfig.ruby} ./bin/rails app:template LOCATION=#{File.expand_path("../../install/install.rb", __dir__)}"
66
end
77

8+
desc "Install node-specific elements for bundlers that use node/yarn."
9+
task :node_shared do
10+
system "#{RbConfig.ruby} ./bin/rails app:template LOCATION=#{File.expand_path("../../install/install_node.rb", __dir__)}"
11+
end
12+
13+
desc "Install Bun"
14+
task bun: "javascript:install:shared" do
15+
system "#{RbConfig.ruby} ./bin/rails app:template LOCATION=#{File.expand_path("../../install/bun/install.rb", __dir__)}"
16+
end
17+
818
desc "Install esbuild"
9-
task esbuild: "javascript:install:shared" do
19+
task esbuild: ["javascript:install:shared", "javascript:install:node_shared"] do
1020
system "#{RbConfig.ruby} ./bin/rails app:template LOCATION=#{File.expand_path("../../install/esbuild/install.rb", __dir__)}"
1121
end
1222

1323
desc "Install rollup.js"
14-
task rollup: "javascript:install:shared" do
24+
task rollup: ["javascript:install:shared", "javascript:install:node_shared"] do
1525
system "#{RbConfig.ruby} ./bin/rails app:template LOCATION=#{File.expand_path("../../install/rollup/install.rb", __dir__)}"
1626
end
1727

1828
desc "Install Webpack"
19-
task webpack: "javascript:install:shared" do
29+
task webpack: ["javascript:install:shared", "javascript:install:node_shared"] do
2030
system "#{RbConfig.ruby} ./bin/rails app:template LOCATION=#{File.expand_path("../../install/webpack/install.rb", __dir__)}"
2131
end
2232
end

0 commit comments

Comments
 (0)