importmap-rails 0.1.3 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
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