importmap-rails 0.1.3 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e9569cf8ada42f3b40596da6bbb24565237a45f3f49108b91470d18715d6fe55
4
- data.tar.gz: 682592d777e3fffa178e4b0f74fc97c83564f4222a881532376a26890faa12da
3
+ metadata.gz: 0ec141d17e7859eac9dae9d91c47f04957f650f2cef7eeae7cc0449b9c92cd89
4
+ data.tar.gz: a2ce05f0a9c340668d407f5c7e2cf64b2fe68f262e567ab94fb1ccc029b10487
5
5
  SHA512:
6
- metadata.gz: 6ff7b0f6d2f5510281a6f540096b03dd7dc760927edf229425c1b5032b93e88ae17276fc9b348c7777cff0eacf39b58e944db26169d11db44c35c75185b89384
7
- data.tar.gz: 9456a86bc8264595860b0e0e03690c82f9fb739f1a5a4ea7850f59f7570daec22c42ba18d6fde9fd5bc5cc76d4b310c6d4e172c4a5dfe8b18ef98d0a7e597dae
6
+ metadata.gz: e692f3214ec9f899ebffd3f239a8f87202811b8098de1f2b2906bcb2558d446801a34fe359b3a94b8b8768220d54918f089ed8149ede3e04708d29f435314872
7
+ data.tar.gz: 89eafbbd709cd3e9eb2e97310c96297465bb935b713e841c8b8f7c9dd4a7d4f59c05b0d5720f0d9087dfbcb3a7e86d17c6d89a6c061622c9b8e6387719707795
data/README.md CHANGED
@@ -13,14 +13,14 @@ There's [native support for import maps in Chrome/Edge 89+](https://caniuse.com/
13
13
  2. Run `./bin/bundle install`
14
14
  3. Run `./bin/rails importmap:install`
15
15
 
16
- By default, all the files in `app/assets/javascripts` and the three major Rails JavaScript libraries are already mapped. You can add more mappings in `config/initializers/assets.rb`.
16
+ By default, all the files in `app/assets/javascripts` and the three major Rails JavaScript libraries are already mapped. You can add more mappings in `config/initializers/importmap.rb`.
17
17
 
18
18
  Note: In order to use JavaScript from Rails frameworks like Action Cable, Action Text, and Active Storage, you must be running Rails 7.0+. This was the first version that shipped with ESM compatible builds of these libraries.
19
19
 
20
20
 
21
21
  ## Usage
22
22
 
23
- The import map is configured programmatically through the `Rails.application.config.importmap.paths` assignment, which by default is setup in `config/initializers/assets.rb` after running the installer. (Note that since this is a config initializer, you must restart your development server after making any changes.)
23
+ The import map is configured programmatically through the `Rails.application.config.importmap` assignment, which by default is setup in `config/initializers/importmap.rb` after running the installer. (Note that since this is a config initializer, you must restart your development server after making any changes.)
24
24
 
25
25
  This programmatically configured import map is inlined in the `<head>` of your application layout using `<%= javascript_importmap_tags %>`, which will setup the JSON configuration inside a `<script type="importmap">` tag. After that, the [es-module-shim](https://github.com/guybedford/es-module-shims) is loaded, and then finally the application entrypoint is imported via `<script type="module">import "application"</script>`. That logical entrypoint, `application`, is mapped in the importmap script tag to the file `app/assets/javascripts/application.js`, which is copied and digested by the asset pipeline.
26
26
 
@@ -31,7 +31,7 @@ It makes sense to use logical names that match the package names used by NPM, su
31
31
 
32
32
  ## Use with Hotwire
33
33
 
34
- This gem was designed for use with [Hotwire](https://hotwired.dev) in mind. The Hotwire gems, like [turbo-rails](https://github.com/hotwired/turbo-rails) and [stimulus-rails](https://github.com/hotwired/stimulus-rails) (both bundled as [hotwire-rails](https://github.com/hotwired/hotwire-rails)), are automatically configured for use with `importmap-rails`. This means you won't have to manually setup the path mapping in `config/initializers/assets.rb`, and instead can simply refer to the logical names directly in your `app/assets/javascripts/application.js`, like so:
34
+ This gem was designed for use with [Hotwire](https://hotwired.dev) in mind. The Hotwire gems, like [turbo-rails](https://github.com/hotwired/turbo-rails) and [stimulus-rails](https://github.com/hotwired/stimulus-rails) (both bundled as [hotwire-rails](https://github.com/hotwired/hotwire-rails)), are automatically configured for use with `importmap-rails`. This means you won't have to manually setup the path mapping in `config/initializers/importmap.rb`, and instead can simply refer to the logical names directly in your `app/assets/javascripts/application.js`, like so:
35
35
 
36
36
  ```js
37
37
  import "@hotwired/turbo-rails"
@@ -41,12 +41,12 @@ import "@hotwired/stimulus-autoloader"
41
41
 
42
42
  ## Use with Skypack (and other CDNs)
43
43
 
44
- Instead of mapping JavaScript modules to files in your application's path, you can also reference them directly from JavaScript CDNs like Skypack. Simply add them to the `config/initializers/assets.rb` with the URL instead of the local path:
44
+ Instead of mapping JavaScript modules to files in your application's path, you can also reference them directly from JavaScript CDNs like Skypack. Simply add them to the `config/initializers/importmap.rb` with the URL instead of the local path:
45
45
 
46
46
  ```ruby
47
- Rails.application.config.importmap.paths.tap do |paths|
48
- paths.asset "trix", path: "https://cdn.skypack.dev/trix"
49
- paths.asset "md5", path: "https://cdn.skypack.dev/md5"
47
+ Rails.application.config.importmap.draw do
48
+ pin "trix", to: "https://cdn.skypack.dev/trix"
49
+ pin "md5", to: "https://cdn.skypack.dev/md5"
50
50
  end
51
51
  ```
52
52
 
@@ -56,7 +56,34 @@ Now you can use these in your application.js entrypoint like you would any other
56
56
  import "trix"
57
57
  import md5 from "md5"
58
58
  console.log(md5("Hash it out"))
59
- ```
59
+ ```
60
+
61
+
62
+ ## Preloading pinned modules
63
+
64
+ To mitigate the waterfall effect where the browser has to load one file after another before it can get to the deepest nested import, we use [modulepreload links](https://developers.google.com/web/updates/2017/12/modulepreload) in [browsers that support it](https://caniuse.com/?search=modulepreload). Pinned modules are preloaded by default, but you can turn this off with `preload: false`.
65
+
66
+ Example:
67
+
68
+ ```ruby
69
+ # config/initializers/importmap.rb
70
+ Rails.application.config.importmap.draw do
71
+ pin "trix", to: "https://cdn.skypack.dev/trix"
72
+ pin "md5", to: "https://cdn.skypack.dev/md5", preload: false
73
+ end
74
+
75
+ # app/views/layouts/application.html.erb
76
+ <%= javascript_importmap_tags %>
77
+
78
+ # will include the following link before the importmap is setup:
79
+ <link rel="modulepreload" href="https://cdn.skypack.dev/trix">
80
+ ...
81
+ ```
82
+
83
+
84
+ ## Caching the import map and preload modules
85
+
86
+ The import map should be cached in production, and is so by default via the `config.importmap.cached` option that will be set to the same value as `config.action_controller.perform_caching`, unless explicitly set differently.
60
87
 
61
88
 
62
89
  ## Expected errors from using the es-module-shim
@@ -1,5 +1,8 @@
1
1
  /* ES Module Shims 0.12.2 */
2
2
  (function () {
3
+ // Temporary hack around https://github.com/guybedford/es-module-shims/issues/148
4
+ if (navigator.userAgent.match("Chrome")) return
5
+
3
6
  const resolvedPromise = Promise.resolve();
4
7
 
5
8
  let baseUrl;
@@ -2,16 +2,17 @@ module Importmap::ImportmapTagsHelper
2
2
  # Setup all script tags needed to use an importmap-powered entrypoint (which defaults to application.js)
3
3
  def javascript_importmap_tags(entry_point = "application")
4
4
  safe_join [
5
+ javascript_importmap_module_preload_tags,
5
6
  javascript_inline_importmap_tag,
6
7
  javascript_importmap_shim_tag,
7
8
  javascript_import_module_tag(entry_point)
8
9
  ], "\n"
9
10
  end
10
11
 
11
- # Generate an inline importmap tag using the passed `importmap_paths` object to produce the JSON map.
12
- # By default, `Rails.application.config.importmap.paths` is used for this object,
13
- def javascript_inline_importmap_tag(importmap_paths = Rails.application.config.importmap.paths)
14
- tag.script(importmap_paths.to_json(self).html_safe, type: "importmap", "data-turbo-track": "reload")
12
+ # Generate an inline importmap tag using the passed `importmap_json` JSON string.
13
+ # By default, `Rails.application.config.importmap.to_json(resolver: self)` is used.
14
+ def javascript_inline_importmap_tag(importmap_json = Rails.application.config.importmap.to_json(resolver: self))
15
+ tag.script(importmap_json.html_safe, type: "importmap", "data-turbo-track": "reload")
15
16
  end
16
17
 
17
18
  # Include the es-module-shim needed to make importmaps work in browsers without native support (like Firefox + Safari).
@@ -23,4 +24,16 @@ module Importmap::ImportmapTagsHelper
23
24
  def javascript_import_module_tag(module_name)
24
25
  tag.script %(import "#{module_name}").html_safe, type: "module"
25
26
  end
27
+
28
+ # Link tags for preloading all modules marked as preload: true in the `importmap`
29
+ # (defaults to Rails.application.config.importmap), such that they'll be fetched
30
+ # in advance by browsers supporting this link type (https://caniuse.com/?search=modulepreload).
31
+ def javascript_importmap_module_preload_tags(importmap = Rails.application.config.importmap)
32
+ javascript_module_preload_tag *importmap.preloaded_module_paths(resolver: self)
33
+ end
34
+
35
+ # Link tag(s) for preloading the JavaScript module residing in `*paths`. Will return one link tag per path element.
36
+ def javascript_module_preload_tag(*paths)
37
+ safe_join(Array(paths).collect { |path| tag.link rel: "modulepreload", href: path }, "\n")
38
+ end
26
39
  end
@@ -1,15 +1,15 @@
1
- require "importmap/paths"
1
+ require "importmap/map"
2
2
 
3
3
  module Importmap
4
4
  class Engine < ::Rails::Engine
5
- config.importmap = ActiveSupport::OrderedOptions.new
6
- config.importmap.paths = Importmap::Paths.new.tap { |paths| paths.assets_in "app/assets/javascripts" }
5
+ config.importmap = Importmap::Map.new
7
6
 
8
7
  config.autoload_once_paths = %W( #{root}/app/helpers )
9
8
 
10
9
  initializer "importmap.assets" do
11
10
  if Rails.application.config.respond_to?(:assets)
12
- Rails.application.config.assets.precompile += %w( es-module-shims )
11
+ Rails.application.config.assets.precompile += %w( es-module-shims.js )
12
+ Rails.application.config.assets.paths << Rails.root.join("app/javascript")
13
13
  end
14
14
  end
15
15
 
@@ -18,5 +18,11 @@ module Importmap
18
18
  helper Importmap::ImportmapTagsHelper
19
19
  end
20
20
  end
21
+
22
+ initializer "importmap.caching" do |app|
23
+ if Rails.application.config.importmap.cached.nil?
24
+ Rails.application.config.importmap.cached = app.config.action_controller.perform_caching
25
+ end
26
+ end
21
27
  end
22
28
  end
@@ -0,0 +1,97 @@
1
+ class Importmap::Map
2
+ attr_reader :files, :directories
3
+ attr_accessor :cached
4
+
5
+ def initialize
6
+ @files, @directories = {}, {}
7
+ end
8
+
9
+ def draw(&block)
10
+ instance_eval(&block)
11
+ end
12
+
13
+ def pin(name, to: nil, preload: true)
14
+ @files[name] = MappedFile.new(name: name, path: to || "#{name}.js", preload: preload)
15
+ end
16
+
17
+ def pin_all_from(path, under: nil, preload: true)
18
+ @directories[path] = MappedDir.new(path: path, under: under, preload: preload)
19
+ end
20
+
21
+ def preloaded_module_paths(resolver:)
22
+ cache_as(:preloaded_module_paths) do
23
+ resolve_asset_paths(expanded_preloading_files_and_directories, resolver: resolver).values
24
+ end
25
+ end
26
+
27
+ def to_json(resolver:)
28
+ cache_as(:json) do
29
+ { "imports" => resolve_asset_paths(expanded_files_and_directories, resolver: resolver) }.to_json
30
+ end
31
+ end
32
+
33
+ private
34
+ MappedFile = Struct.new(:name, :path, :preload, keyword_init: true)
35
+ MappedDir = Struct.new(:path, :under, :preload, keyword_init: true)
36
+
37
+ def cache_as(name)
38
+ if (cached && result = instance_variable_get("@cached_#{name}"))
39
+ result
40
+ else
41
+ instance_variable_set("@cached_#{name}", yield)
42
+ end
43
+ end
44
+
45
+ def resolve_asset_paths(paths, resolver:)
46
+ paths.transform_values do |mapping|
47
+ begin
48
+ resolver.asset_path(mapping.path)
49
+ rescue Sprockets::Rails::Helper::AssetNotFound
50
+ Rails.logger.warn "Importmap skipped missing path: #{mapping.path}"
51
+ nil
52
+ end
53
+ end.compact
54
+ end
55
+
56
+ def expanded_preloading_files_and_directories
57
+ expanded_files_and_directories.select { |name, mapping| mapping.preload }
58
+ end
59
+
60
+ def expanded_files_and_directories
61
+ @files.dup.tap { |expanded| expand_directories_into expanded }
62
+ end
63
+
64
+ def expand_directories_into(paths)
65
+ @directories.values.each do |mapping|
66
+ if (absolute_path = absolute_root_of(mapping.path)).exist?
67
+ find_javascript_files_in_tree(absolute_path).each do |filename|
68
+ module_filename = filename.relative_path_from(absolute_path)
69
+ module_name = module_name_from(module_filename, mapping.under)
70
+ module_path = mapping.under ? absolute_path.basename.join(module_filename).to_s : module_filename.to_s
71
+
72
+ paths[module_name] = MappedFile.new(name: module_name, path: module_path, preload: mapping.preload)
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ # Strip off the extension, /index, or any versioning data for an absolute module name.
79
+ def module_name_from(filename, under)
80
+ filename_without_ext = filename.to_s.remove(filename.extname)
81
+
82
+ if filename_without_ext == "index" && under
83
+ under
84
+ else
85
+ module_name = filename_without_ext.split("@").first
86
+ under ? "#{under}/#{module_name}" : module_name
87
+ end
88
+ end
89
+
90
+ def find_javascript_files_in_tree(path)
91
+ Dir[path.join("**/*.js{,m}")].collect { |file| Pathname.new(file) }.select(&:file?)
92
+ end
93
+
94
+ def absolute_root_of(path)
95
+ (pathname = Pathname.new(path)).absolute? ? pathname : Rails.root.join(path)
96
+ end
97
+ end
@@ -1,3 +1,3 @@
1
1
  module Importmap
2
- VERSION = "0.1.3"
2
+ VERSION = "0.2.3"
3
3
  end
@@ -9,8 +9,8 @@ else
9
9
  end
10
10
 
11
11
  say "Create application.js module as entrypoint"
12
- create_file Rails.root.join("app/assets/javascripts/application.js") do <<-JS
13
- // Configure your import map in config/initializers/assets.rb
12
+ create_file Rails.root.join("app/javascript/application.js") do <<-JS
13
+ // Configure your import map in config/initializers/importmap.rb
14
14
 
15
15
  // import "@rails/actioncable"
16
16
  // import "@rails/activestorage"
@@ -18,27 +18,24 @@ JS
18
18
  end
19
19
 
20
20
  say "Ensure JavaScript files are in the asset pipeline manifest"
21
- append_to_file Rails.root.join("app/assets/config/manifest.js"), %(//= link_tree ../javascripts .js\n)
21
+ append_to_file Rails.root.join("app/assets/config/manifest.js"), %(//= link_tree ../../../javascript .js\n)
22
22
 
23
- say "Configure importmap paths in config/initializers/assets.rb"
24
- append_to_file Rails.root.join("config/initializers/assets.rb") do <<-RUBY
23
+ say "Configure importmap paths in config/initializers/importmap.rb"
24
+ create_file Rails.root.join("config/initializers/importmap.rb") do <<-RUBY
25
+ Rails.application.config.importmap.draw do
26
+ pin "application"
25
27
 
26
- # Configure import map beyond the default of having all files in app/assets/javascripts mapped.
27
- Rails.application.config.importmap.paths.tap do |paths|
28
- # Match libraries with their NPM package names for possibility of later porting.
29
- # Ensure that libraries listed in the path have been linked in the asset pipeline manifest or precompiled.
30
- paths.asset "@rails/actioncable", path: "actioncable.esm.js"
31
- paths.asset "@rails/activestorage", path: "activestorage.esm.js"
32
- paths.asset "@rails/actiontext", path: "actiontext.js"
33
- paths.asset "trix"
28
+ # Use libraries available via the asset pipeline (locally or via gems).
29
+ # pin "@rails/actioncable", to: "actioncable.esm.js"
30
+ # pin "@rails/activestorage", to: "activestorage.esm.js"
34
31
 
35
- # Use libraries directly from JavaScript CDNs
36
- # paths.asset "vue", path: "https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.esm.browser.js"
37
- # paths.asset "d3", path: "https://cdn.skypack.dev/pin/d3@v7.0.0-03vFl9bie0TSesDkWTJV/mode=imports/optimized/d3.js"
32
+ # Use libraries directly from JavaScript CDNs (see https://www.skypack.dev, https://cdnjs.com, https://www.jsdelivr.com/esm)
33
+ # pin "vue", to: "https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.esm.browser.js", preload: true
34
+ # pin "d3", to: "https://cdn.skypack.dev/pin/d3@v7.0.0-03vFl9bie0TSesDkWTJV/mode=imports/optimized/d3.js", preload: true
38
35
 
39
- # Map vendored modules by first adding the following to app/assets/config/manifest.js:
36
+ # Pin vendored modules by first adding the following to app/assets/config/manifest.js:
40
37
  # //= link_tree ../../../vendor/assets/javascripts .js
41
- # paths.assets_in "vendor/assets/javascripts"
38
+ # pin_all_from "vendor/assets/javascripts"
42
39
  end
43
40
  RUBY
44
41
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: importmap-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Heinemeier Hansson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-08-14 00:00:00.000000000 Z
11
+ date: 2021-08-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -38,7 +38,7 @@ files:
38
38
  - app/helpers/importmap/importmap_tags_helper.rb
39
39
  - lib/importmap-rails.rb
40
40
  - lib/importmap/engine.rb
41
- - lib/importmap/paths.rb
41
+ - lib/importmap/map.rb
42
42
  - lib/importmap/version.rb
43
43
  - lib/install/install.rb
44
44
  - lib/shim.js
@@ -1,63 +0,0 @@
1
- class Importmap::Paths
2
- attr_reader :files, :directories
3
-
4
- def initialize
5
- @files = {}
6
- @directories = {}
7
- end
8
-
9
- def asset(name, path: nil)
10
- @files[name] = path || "#{name}.js"
11
- end
12
-
13
- def assets_in(path, append_base_path: false)
14
- @directories[path] = append_base_path
15
- end
16
-
17
- def to_json(resolver)
18
- { "imports" => map_to_asset_paths(resolver) }.to_json
19
- end
20
-
21
- private
22
- def map_to_asset_paths(resolver)
23
- expanded_files_and_directories.transform_values do |path|
24
- begin
25
- resolver.asset_path(path)
26
- rescue Sprockets::Rails::Helper::AssetNotFound
27
- Rails.logger.warn "Importmap skipped missing asset: #{path}"
28
- nil
29
- end
30
- end.compact
31
- end
32
-
33
- def expanded_files_and_directories
34
- @files.dup.tap { |expanded| expand_directories_into expanded }
35
- end
36
-
37
- def expand_directories_into(paths)
38
- @directories.each do |(path, append_base_path)|
39
- if (absolute_path = absolute_root_of(path)).exist?
40
- find_javascript_files_in_tree(absolute_path).each do |filename|
41
- module_filename = filename.relative_path_from(absolute_path)
42
- module_name = module_name_from(module_filename)
43
- module_path = append_base_path ? absolute_path.basename.join(module_filename).to_s : module_filename.to_s
44
-
45
- paths[module_name] = module_path
46
- end
47
- end
48
- end
49
- end
50
-
51
- # Strip off the extension, /index, or any versioning data for an absolute module name.
52
- def module_name_from(filename)
53
- filename.to_s.remove(filename.extname).remove("/index").split("@").first
54
- end
55
-
56
- def find_javascript_files_in_tree(path)
57
- Dir[path.join("**/*.js{,m}")].collect { |file| Pathname.new(file) }.select(&:file?)
58
- end
59
-
60
- def absolute_root_of(path)
61
- (pathname = Pathname.new(path)).absolute? ? pathname : Rails.root.join(path)
62
- end
63
- end