diff --git a/lib/importmap/engine.rb b/lib/importmap/engine.rb index 2828868..46ceb2b 100755 --- a/lib/importmap/engine.rb +++ b/lib/importmap/engine.rb @@ -10,6 +10,7 @@ class Engine < ::Rails::Engine config.importmap.sweep_cache = Rails.env.development? || Rails.env.test? config.importmap.cache_sweepers = [] config.importmap.rescuable_asset_errors = [] + config.importmap.accept = %w( js ) config.autoload_once_paths = %W( #{root}/app/helpers ) diff --git a/lib/importmap/map.rb b/lib/importmap/map.rb index 60b7dbd..e7e94ef 100644 --- a/lib/importmap/map.rb +++ b/lib/importmap/map.rb @@ -26,7 +26,7 @@ def draw(path = nil, &block) def pin(name, to: nil, preload: false) clear_cache - @packages[name] = MappedFile.new(name: name, path: to || "#{name}.js", preload: preload) + @packages[name] = MappedFile.new(name: name, path: to || javascript_filename(name), preload: preload) end def pin_all_from(dir, under: nil, to: nil, preload: false) @@ -73,10 +73,8 @@ def digest(resolver:) # and test to ensure the map caches are reset when javascript files are changed. def cache_sweeper(watches: nil) if watches - @cache_sweeper = - Rails.application.config.file_watcher.new([], Array(watches).collect { |dir| [ dir.to_s, "js"] }.to_h) do - clear_cache - end + watches = Array(watches).collect { |dir| [ dir.to_s, accepted_extensions] }.to_h + @cache_sweeper = Rails.application.config.file_watcher.new([], watches) { clear_cache } else @cache_sweeper end @@ -137,7 +135,7 @@ def expanded_packages_and_directories def expand_directories_into(paths) @directories.values.each do |mapping| if (absolute_path = absolute_root_of(mapping.dir)).exist? - find_javascript_files_in_tree(absolute_path).each do |filename| + find_accepted_files_in_tree(absolute_path).each do |filename| module_filename = filename.relative_path_from(absolute_path) module_name = module_name_from(module_filename, mapping) module_path = module_path_from(module_filename, mapping) @@ -149,18 +147,30 @@ def expand_directories_into(paths) end def module_name_from(filename, mapping) - [ mapping.under, filename.to_s.remove(filename.extname).remove(/\/?index$/).presence ].compact.join("/") + [ mapping.under, filename.to_s.remove(accepted_extensions_pattern).remove(/\/?index$/).presence ].compact.join("/") end def module_path_from(filename, mapping) - [ mapping.path || mapping.under, filename.to_s ].compact.join("/") + [ mapping.path || mapping.under, javascript_filename(filename.to_s) ].compact.join("/") end - def find_javascript_files_in_tree(path) - Dir[path.join("**/*.js{,m}")].collect { |file| Pathname.new(file) }.select(&:file?) + def find_accepted_files_in_tree(path) + Dir[path.join("**/*.{#{accepted_extensions.join(',')}}")].map(&Pathname.method(:new)).select(&:file?) end def absolute_root_of(path) (pathname = Pathname.new(path)).absolute? ? pathname : Rails.root.join(path) end + + def accepted_extensions + Rails.application.config.importmap.accept + end + + def accepted_extensions_pattern + /\.(#{accepted_extensions.map(&Regexp.method(:escape)).join('|')})\z/ + end + + def javascript_filename(name) + "#{name.remove(accepted_extensions_pattern)}.js" + end end diff --git a/test/cache_sweeper_test.rb b/test/cache_sweeper_test.rb new file mode 100644 index 0000000..0ef8b7b --- /dev/null +++ b/test/cache_sweeper_test.rb @@ -0,0 +1,43 @@ +require "test_helper" + +class CacheSweeperTest < ActiveSupport::TestCase + + test "sweep is triggered when asset with extra extension changes" do + previous_accept = Rails.application.config.importmap.accept + Rails.application.config.importmap.accept += %w[jsx] + + @importmap = Importmap::Map.new.tap do |map| + map.draw do + pin "application" + pin "components/Clock" + end + end + + @importmap.cache_sweeper watches: %w[app/javascript vendor/javascript].map(&Rails.root.method(:join)) + + resolver = MockResolver.new(%r{components/Clock\.js\z}) + imports = generate_imports(resolver: resolver) + touch_asset 'components/Clock.jsx' + new_imports = generate_imports(resolver: resolver) + + assert_not_nil imports["components/Clock"] + assert_not_nil new_imports["components/Clock"] + assert_not_nil imports["application"] + assert_not_nil new_imports["application"] + assert_not_equal imports["components/Clock"], new_imports["components/Clock"] + assert_equal imports["application"], new_imports["application"] + ensure + Rails.application.config.importmap.accept = previous_accept + end + + private + def touch_asset(name) + FileUtils.touch Rails.root.join('app', 'javascript', name) + sleep 3 + @importmap.cache_sweeper.execute_if_updated + end + + def generate_imports(resolver: ApplicationController.helpers) + JSON.parse(@importmap.to_json(resolver: resolver))["imports"] + end +end diff --git a/test/dummy/app/javascript/components/Clock.jsx b/test/dummy/app/javascript/components/Clock.jsx new file mode 100644 index 0000000..fc072f8 --- /dev/null +++ b/test/dummy/app/javascript/components/Clock.jsx @@ -0,0 +1,12 @@ +import { Component } from "react"; + +export default class Clock extends Component { + render() { + return ( +
+

UNIX Clock

+

The current UNIX date is {Date.now()}.

+
+ ); + } +} diff --git a/test/dummy/app/javascript/components/index.jsx b/test/dummy/app/javascript/components/index.jsx new file mode 100644 index 0000000..ee65bec --- /dev/null +++ b/test/dummy/app/javascript/components/index.jsx @@ -0,0 +1,7 @@ +import { render } from "react-dom"; +import Clock from "components/Clock"; + +render( + , + document.getElementById('root') +); diff --git a/test/importmap_test.rb b/test/importmap_test.rb index 1bb1c94..eaf432d 100644 --- a/test/importmap_test.rb +++ b/test/importmap_test.rb @@ -13,6 +13,7 @@ def setup pin_all_from "app/javascript/spina/controllers", under: "controllers/spina", preload: true pin_all_from "app/javascript/spina/controllers", under: "controllers/spina", to: "spina/controllers", preload: true pin_all_from "app/javascript/helpers", under: "helpers", preload: true + pin_all_from "app/javascript/components", under: "components" pin_all_from "lib/assets/javascripts", preload: true end end @@ -71,6 +72,27 @@ def setup assert_no_match /application/, preloading_module_paths end + test "jsx files are mapped to js when importmap accepts jsx" do + previous_accept = Rails.application.config.importmap.accept + Rails.application.config.importmap.accept += %w[jsx] + @importmap = Importmap::Map.new.tap do |map| + map.draw do + pin "application" + pin_all_from "app/javascript/controllers", under: "controllers", preload: true + pin_all_from "app/javascript/components", under: "components" + end + end + importmap_json = generate_importmap_json(resolver: MockResolver.new(%r{components/(Clock|index)\.js\z})) + assert_match %r|assets/components/index-.*\.js|, importmap_json["imports"]["components"] + assert_match %r|assets/components/Clock-.*\.js|, importmap_json["imports"]["components/Clock"] + ensure + Rails.application.config.importmap.accept = previous_accept + end + + test "jsx files are not mapped when importmap doesn't accept jsx" do + assert_nil generate_importmap_json["imports"]["components/Clock"] + end + test "digest" do assert_match /^\w{40}$/, @importmap.digest(resolver: ApplicationController.helpers) end @@ -98,7 +120,8 @@ def setup end private - def generate_importmap_json - JSON.parse @importmap.to_json(resolver: ApplicationController.helpers) + + def generate_importmap_json(resolver: ApplicationController.helpers) + JSON.parse @importmap.to_json(resolver: resolver) end end diff --git a/test/reloader_test.rb b/test/reloader_test.rb index e2d4dcd..6575cce 100644 --- a/test/reloader_test.rb +++ b/test/reloader_test.rb @@ -25,6 +25,6 @@ class ReloaderTest < ActiveSupport::TestCase private def touch_config FileUtils.touch(@config) - sleep 1 + sleep 2 end end diff --git a/test/test_helper.rb b/test/test_helper.rb index 5519045..cd6b3f7 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -14,3 +14,22 @@ ActiveSupport::TestCase.file_fixture_path = ActiveSupport::TestCase.fixture_path + "/files" ActiveSupport::TestCase.fixtures :all end + +MockResolver = Struct.new(:pattern) do + def path_to_asset(path) + if path =~ pattern + digest = Digest::SHA256.hexdigest(source_file(path).mtime.to_s) + "/assets/" + path.sub(/\.js\z/, "-#{digest}.js") + else + ApplicationController.helpers.asset_path(path) + end + end + + def root + Rails.root.join('app', 'javascript') + end + + def source_file(path) + root.join("#{path}x") + end +end