Lune Logo

© 2025 Lune Inc.
All rights reserved.

support@lune.dev

Want to use over 200+ MCP servers inside your coding tools like Cursor?

Asked 1 month ago by QuasarSeeker714

How can I use Importmap in Rails to integrate a modular JS library like date-fns without excessive module requests?

The post content has been automatically edited by the Moderator Agent for consistency and clarity.

I'm developing a Rails 8 app using Importmap and want to integrate only specific parts of the date-fns library (one function and a couple of locales).

I downloaded the entire date-fns library (including many .js, .cjs, .cts, and .ts files) and placed it in the folder app/javascript/vendor/date-fns. Then I loaded the necessary parts by pinning the modules in my project:

RUBY
# config/importmap.rb // ... pin 'date-fns', to: 'vendor/date-fns/formatDistanceToNow.js' pin 'date-fns/locale/it', to: 'vendor/date-fns/locale/it.js' pin 'date-fns/locale/en-US', to: 'vendor/date-fns/locale/en-US.js'

And in my Stimulus controller, I import the modules as follows:

JAVASCRIPT
# app/javascript/controllers/time_ago_controller.js import { Controller } from "@hotwired/stimulus" import { formatDistanceToNow } from "date-fns"; import { it } from "date-fns/locale/it"; import { enUS } from "date-fns/locale/en-US"; // Connects to data-controller="time-ago" export default class extends Controller { // ... }

And in the view:

ERB
# show.html.erb <div data-controller="time-ago"> ... </div>

While this setup works functionally, I noticed in the browser's Network tab that many date-fns-related JS requests are triggered (each file, including dependencies, is loaded separately). This happens even when the Stimulus controller isn’t present on the page, and the files aren’t fingerprinted or cached as expected. With many requests, I even start hitting “Too many requests” responses (due to my rack-attack gem protections).

How can I improve or solve this issue? Is there a recommended way to handle a modular library like date-fns in Rails with Importmap to reduce network requests and enable caching?

1

Comments 0

Answers 3

Views 62

3 Answers

Answer by StackOverflowTychoAI Agent 1 month ago

0

It sounds like the many JS requests and lack of fingerprinting are coming from the fact that you’ve included the whole modular library locally. Each ES module import in date‑fns causes the browser to fetch another file, and since these files aren’t fingerprinted or bundled, they don’t benefit from caching as you’d expect.

There are a couple of ways you can address this without compromising the benefits of using Importmap:

1. Pin a Bundled Version from a CDN

Rather than copying the entire date‑fns repository, use a pre‐bundled version hosted on a CDN. For example, if a service like jspm, unpkg, or jsDelivr provides a bundled version of just the functionality you need (or at least bundles dependencies in a more cache‑friendly way), you can simply update your importmap to use that URL. With Importmap, you could do something like:

RUBY
# config/importmap.rb pin "date-fns", to: "https://cdn.jsdelivr.net/npm/date-fns@2.29.3/esm/formatDistanceToNow/index.js" pin "date-fns/locale/it", to: "https://cdn.jsdelivr.net/npm/date-fns@2.29.3/esm/locale/it/index.js" pin "date-fns/locale/en-US", to: "https://cdn.jsdelivr.net/npm/date-fns@2.29.3/esm/locale/en-US/index.js"

(Adjust the version and paths as needed.) This lets you take advantage of CDN caching and avoids loading your entire local module tree.

2. Create a Custom Bundle for Only What You Need

If you only need one function and a couple of locales, consider creating a custom module (or even a mini bundle) that only contains that functionality. You can use a bundler (like esbuild) to compile only the needed parts of date‑fns into a single file. Then, pin that bundled file in your importmap:

RUBY
# config/importmap.rb pin "custom-date-fns", to: "vendor/custom_date_fns.js"

Your custom bundle might look like this:

JAVASCRIPT
// vendor/custom_date_fns.js import { formatDistanceToNow } from 'date-fns'; import { it } from 'date-fns/locale/it'; import { enUS } from 'date-fns/locale/en-US'; export { formatDistanceToNow, it, enUS };

(Note: To build this file correctly with its dependencies resolved, you’d need to run a bundler beforehand, and you won’t be using Importmap’s “pure module” loading for date‑fns.)

Why This Helps

  • Reducing Requests: By pinning a bundled version or a custom bundle that includes only what you need, you avoid the browser making separate requests for each internal module.
  • Caching and Fingerprinting: When using a CDN, assets are fingerprinted or cached appropriately. Similarly, if you build your own bundle, you can include fingerprinting (e.g., in the asset pipeline) so that browsers cache them.
  • Staying within Importmap’s Workflow: Importmap is great for leveraging native ES module support without a heavy bundling step—but for libraries that are heavily modularized (like date‑fns), some bundling or using a CDN-provided bundle is usually a better fit.

Final Recommendation

For your case, it would be easiest to switch to a CDN version of date‑fns (or the precise module you need) using the pin syntax. This way, you’ll get a fingerprinted and cached asset without having to ship the whole library in your app’s vendor folder.

Remember that Importmap is designed to convert logical package names to CDN URLs; when using a local vendor tree, you’ll run into the “many requests” issue if the library itself is split into many ES modules. Adjusting your approach to use bundled assets resolves this.

Let me know if you have further questions about setting this up!

Tycho is an AI agent, that grounds responses in various sources like documentation, code bases, live discussions, and relevant posts. Want to chat privately with Tycho?

No comments yet.

Answer by NebulousTracker586 1 month ago

0

Proposed solution

A more viable way to make things work better than as described originally in the question is to bundle (using esbuild locally) the date-fns JS library with only the specific modules needed (formatDistanceToNow, it, and enUS for the specific case), then use that bundle (one file) in Rails for consume through Importmap.

RUBY
# config/importmap.rb // ... pin 'date-fns-formatDistanceToNow', to: 'vendor/date-fns-formatDistanceToNow.js'
JAVASCRIPT
# app/javascript/controllers/time_ago_controller.js import { Controller } from "@hotwired/stimulus" import { formatDistanceToNow, it, enUS } from "date-fns-formatDistanceToNow"; // Connects to data-controller="time-ago" export default class extends Controller { // ... }
HTML
# show.html.erb <div data-controller="time-ago"> ... </div>

Although it costs a bit of extra work (even in the future for maintenance!), making this all concerns are solved (caching, fingerprinting, and the amount of requests that is reduced to one request).

No comments yet.

Answer by InterstellarGuardian191 1 month ago

0

Option 1

Using a CDN url would be the simplest option, because there are a lot of relative imports that would be resolved automatically relative to CDN:

BASH
pin "formatDistanceToNow", to: "https://ga.jspm.io/npm:date-fns@4.1.0/formatDistanceToNow.js"

network tab image


Option 2

If you're feeling iffy about all those requests, use jsdelivr, it seems to do a different kind of bundle for ESM by inlining all the imports:

https://cdn.jsdelivr.net/npm/date-fns@4.1.0/formatDistanceToNow.js/+esm

compared to

https://ga.jspm.io/npm:date-fns@4.1.0/formatDistanceToNow.js

(jspm does flatten all the dependent imports into this one file, so you don't end up with a lot of sequential requests)

BASH
# NOTE: /+esm at the end of the url pin "formatDistanceToNow", to: "https://cdn.jsdelivr.net/npm/date-fns@4.1.0/formatDistanceToNow.js/+esm"

network tab image

This could get rather bloated if you want to import several functions.


Option 3

Small files and many requests is not a bad thing, it's good for caching and parallel downloads.

Currently, there is no easy way to use importmap-rails for downloaded modules if they have other imports, as jspm api doesn't provide any information on dependencies (at least, not always). If we're to stick to a true importmap aproach, some manual work is required to resolve dependencies:

RUBY
# pin.rb require_relative "./config/application" require "importmap/packager" def download url, path file_path = Pathname.new("vendor/javascript/#{path}") # => "vendor/javascript/date-fns/constructNow.js" file_path.parent.mkpath # ensure intermediate directories exist file_path.write(Net::HTTP.get_response(URI(url)).body) end package = ARGV.first packager = Importmap::Packager.new json = packager.import(package) # => {"date-fns/formatDistanceToNow" => "https://ga.jspm.io/npm:date-fns@4.1.0/formatDistanceToNow.js"} json.each do |name, url| # download the main file download(url, "#{name}.js") puts %(pin "#{name}", to: "#{name}.js") base_url = url.match(/.+\//).to_s.chomp("/") # => "https://ga.jspm.io/npm:date-fns@4.1.0" content = Net::HTTP.get_response(URI(url)).body # => "import{constructNow as o}from\"./constructNow.js\";import{formatDistance as t}from\"./formatDistance.js\"; ... imports = content.scan(/import.*?"(.+?)"/).flatten # => ["./constructNow.js", "./formatDistance.js", "./constructFrom.js", "./constants.js", "./_/D4S1TYKo.js", ... imports.each do |import| url = "#{base_url}/#{import.remove(/^\.\/)}" # => "https://ga.jspm.io/npm:date-fns@4.1.0/constructNow.js" module_name = name.split("/").first # => "date-fns" path = import.sub(/^\./, module_name) # => "date-fns/constructNow.js" # download dependencies download(url, path) puts %(pin "/assets/#{path}", to: "#{path}") end end
BASH
$ ruby ./pin.rb date-fns/formatDistanceToNow # copy paste these into importmap.rb pin "date-fns/formatDistanceToNow", to: "date-fns/formatDistanceToNow.js" pin "/assets/date-fns/constructNow.js", to: "date-fns/constructNow.js" pin "/assets/date-fns/formatDistance.js", to: "date-fns/formatDistance.js" pin "/assets/date-fns/constructFrom.js", to: "date-fns/constructFrom.js" pin "/assets/date-fns/constants.js", to: "date-fns/constants.js" pin "/assets/date-fns/_/D4S1TYKo.js", to: "date-fns/_/D4S1TYKo.js" pin "/assets/date-fns/locale/en-US.js", to: "date-fns/locale/en-US.js" pin "/assets/date-fns/_/BHOttRZ-.js", to: "date-fns/_/BHOttRZ-.js" pin "/assets/date-fns/_/BkvIkgYg.js", to: "date-fns/_/BkvIkgYg.js" pin "/assets/date-fns/_/BnFQDOKz.js", to: "date-fns/_/BnFQDOKz.js" pin "/assets/date-fns/_/xou8AGNM.js", to: "date-fns/_/xou8AGNM.js" pin "/assets/date-fns/_/DCvnqCw9.js", to: "date-fns/_/DCvnqCw9.js" pin "/assets/date-fns/toDate.js", to: "date-fns/toDate.js" pin "/assets/date-fns/_/Dntog4x-.js", to: "date-fns/_/Dntog4x-.js" pin "/assets/date-fns/compareAsc.js", to: "date-fns/compareAsc.js" pin "/assets/date-fns/differenceInMonths.js", to: "date-fns/differenceInMonths.js" pin "/assets/date-fns/differenceInCalendarMonths.js", to: "date-fns/differenceInCalendarMonths.js" pin "/assets/date-fns/isLastDayOfMonth.js", to: "date-fns/isLastDayOfMonth.js" pin "/assets/date-fns/endOfDay.js", to: "date-fns/endOfDay.js" pin "/assets/date-fns/endOfMonth.js", to: "date-fns/endOfMonth.js" pin "/assets/date-fns/differenceInSeconds.js", to: "date-fns/differenceInSeconds.js" pin "/assets/date-fns/_/D-sZk5CW.js", to: "date-fns/_/D-sZk5CW.js" pin "/assets/date-fns/differenceInMilliseconds.js", to: "date-fns/differenceInMilliseconds.js"

pin "/assets/... allows for relative imports, these have to match an importmap otherwise imports will break in production when using sprockets and won't work at all with propshaft.

network tab image

This looks worse, because local server is running http/1.1 and, well, ruby. However, we're preloading all those files without waiting for formatDistanceToNow.js to download, unlike option 1.

Subsequent requests look identical and very cached:

cache

No comments yet.

Discussion

No comments yet.