opal-rails 2.0.3 → 3.0.0

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.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/build.yml +39 -4
  3. data/.gitignore +5 -0
  4. data/Appraisals +20 -21
  5. data/CHANGELOG.md +36 -0
  6. data/Gemfile +2 -2
  7. data/PORTING.md +140 -0
  8. data/README.md +151 -88
  9. data/Rakefile +16 -2
  10. data/app/helpers/opal_helper.rb +9 -28
  11. data/bin/sandbox +3 -2
  12. data/bin/sandbox-setup +2 -0
  13. data/gemfiles/{rails_7_0_opal_1_0.gemfile → rails_7_0_opal_1_8.gemfile} +2 -2
  14. data/gemfiles/{rails_7_0_opal_1_3.gemfile → rails_7_0_opal_master.gemfile} +2 -2
  15. data/gemfiles/{rails_6_0_opal_1_0.gemfile → rails_8_0_opal_1_8.gemfile} +3 -3
  16. data/gemfiles/rails_8_0_opal_master.gemfile +10 -0
  17. data/gemfiles/{rails_6_1_opal_1_0.gemfile → rails_8_1_opal_1_8.gemfile} +3 -3
  18. data/gemfiles/rails_8_1_opal_master.gemfile +10 -0
  19. data/lib/generators/opal/assets/assets_generator.rb +11 -1
  20. data/lib/generators/opal/assets/templates/{javascript.js.rb → asset.rb.tt} +3 -6
  21. data/lib/generators/opal/install/install_generator.rb +160 -9
  22. data/lib/generators/opal/install/templates/{application.js.rb → application.rb} +4 -4
  23. data/lib/generators/opal/install/templates/dev.tt +8 -0
  24. data/lib/generators/opal/install/templates/{initializer.rb → initializer.rb.tt} +8 -0
  25. data/lib/opal/rails/builder_runner.rb +126 -0
  26. data/lib/opal/rails/engine.rb +11 -9
  27. data/lib/opal/rails/entrypoints_resolver.rb +81 -0
  28. data/lib/opal/rails/errors.rb +11 -0
  29. data/lib/opal/rails/file_watcher.rb +58 -0
  30. data/lib/opal/rails/haml6_filter.rb +6 -6
  31. data/lib/opal/rails/haml_filter.rb +3 -6
  32. data/lib/opal/rails/legacy_upgrade_warning.rb +95 -0
  33. data/lib/opal/rails/outputs_manifest.rb +82 -0
  34. data/lib/opal/rails/path_setup.rb +28 -0
  35. data/lib/opal/rails/task_hooks.rb +49 -0
  36. data/lib/opal/rails/version.rb +1 -1
  37. data/lib/opal/rails/watch_runner.rb +173 -0
  38. data/lib/opal/rails.rb +7 -0
  39. data/lib/tasks/opal.rake +48 -0
  40. data/opal-rails.gemspec +23 -14
  41. data/spec/end_to_end/full_lifecycle_spec.rb +377 -0
  42. data/spec/helpers/opal_helper_spec.rb +27 -34
  43. data/spec/integration/source_map_spec.rb +6 -8
  44. data/spec/opal/assets_generator_spec.rb +31 -0
  45. data/spec/opal/install_generator_spec.rb +140 -0
  46. data/spec/opal/rails/build_task_spec.rb +116 -0
  47. data/spec/opal/rails/builder_runner_spec.rb +151 -0
  48. data/spec/opal/rails/clobber_task_spec.rb +55 -0
  49. data/spec/opal/rails/entrypoints_resolver_spec.rb +50 -0
  50. data/spec/opal/rails/haml_filter_spec.rb +41 -0
  51. data/spec/opal/rails/legacy_upgrade_warning_spec.rb +94 -0
  52. data/spec/opal/rails/outputs_manifest_spec.rb +44 -0
  53. data/spec/opal/rails/path_setup_spec.rb +71 -0
  54. data/spec/opal/rails/task_hooks_spec.rb +61 -0
  55. data/spec/opal/rails/watch_runner_spec.rb +283 -0
  56. data/spec/opal/rails/watch_task_spec.rb +23 -0
  57. data/spec/spec_helper.rb +16 -7
  58. data/spec/support/browser_support_spec.rb +36 -0
  59. data/spec/support/capybara.rb +61 -0
  60. data/spec/support/reset_assets_cache.rb +9 -1
  61. data/spec/support/test_app.rb +23 -2
  62. data/test_apps/app/application_controller.rb +23 -32
  63. data/test_apps/app/assets/builds/.keep +0 -0
  64. data/test_apps/app/assets/config/manifest.js +3 -1
  65. data/test_apps/app/assets/images/.keep +0 -0
  66. data/test_apps/app/opal/application.rb +5 -0
  67. data/test_apps/app/{assets/javascripts/source_map_example.js.rb → opal/source_map_example.rb} +1 -2
  68. data/test_apps/app/opal/with_assignments.js.rb +8 -0
  69. data/test_apps/rails.rb +19 -5
  70. metadata +196 -50
  71. data/gemfiles/rails_6_0_opal_1_1.gemfile +0 -9
  72. data/gemfiles/rails_6_0_opal_1_3.gemfile +0 -10
  73. data/gemfiles/rails_6_0_opal_1_7.gemfile +0 -10
  74. data/gemfiles/rails_6_1_opal_1_1.gemfile +0 -9
  75. data/gemfiles/rails_6_1_opal_1_3.gemfile +0 -10
  76. data/gemfiles/rails_6_1_opal_1_7.gemfile +0 -10
  77. data/gemfiles/rails_7_0_opal_1_7.gemfile +0 -10
  78. data/lib/opal/rails/haml5_filter.rb +0 -28
  79. data/test_apps/app/assets/javascripts/application.js.rb +0 -7
  80. data/test_apps/app/assets/javascripts/bar.rb +0 -3
  81. data/test_apps/app/assets/javascripts/foo.js.rb +0 -3
@@ -1,14 +1,165 @@
1
1
  class Opal::InstallGenerator < Rails::Generators::Base
2
2
  source_root File.expand_path('templates', __dir__)
3
3
 
4
- def configure_sprockets
5
- append_to_file 'app/assets/config/manifest.js', '//= link_directory ../javascript .js'
6
- template "application.js.rb", "app/assets/javascript/application.js.rb"
7
- template "initializer.rb", "config/initializers/opal.rb"
8
-
9
- # Add the javascript tag to the application head tag
10
- gsub_file 'app/views/layouts/application.html.erb', %r{(\n *)</head>},
11
- '\1 <%= javascript_include_tag "application", "data-turbolinks-track": "reload" %>' \
12
- '\1</head>'
4
+ def create_opal_files
5
+ create_application_entrypoint
6
+ template 'initializer.rb.tt', 'config/initializers/opal.rb'
7
+ empty_directory 'app/assets/builds'
8
+ create_file 'app/assets/builds/.keep' unless destination_file_exist?('app/assets/builds/.keep')
9
+ ensure_manifest_build_links
10
+ ensure_test_asset_debug
11
+ ensure_gitignore_entries
12
+ ensure_procfile_dev
13
+ ensure_bin_dev
14
+ ensure_javascript_include_tag
15
+ end
16
+
17
+ private
18
+
19
+ def create_application_entrypoint
20
+ return unless generate_application_entrypoint?
21
+
22
+ template 'application.rb', application_entrypoint_path
23
+ end
24
+
25
+ def opal_source_path
26
+ destination_directory_exist?('app/assets/opal') ? 'app/assets/opal' : 'app/opal'
27
+ end
28
+
29
+ def application_entrypoint_path
30
+ File.join(opal_source_path, 'application.rb')
31
+ end
32
+
33
+ def generate_application_entrypoint?
34
+ return false if destination_file_exist?(application_entrypoint_path)
35
+
36
+ existing_top_level_entrypoints.empty?
37
+ end
38
+
39
+ def application_entrypoint_available?
40
+ generate_application_entrypoint? || destination_file_exist?(application_entrypoint_path)
41
+ end
42
+
43
+ def entrypoints_literal
44
+ return ':all' if bulk_entrypoints_layout?
45
+
46
+ "{ '#{application_logical_name}' => 'application.rb' }"
47
+ end
48
+
49
+ def application_logical_name
50
+ application_asset_name_reserved? ? 'opal' : 'application'
51
+ end
52
+
53
+ def application_asset_name_reserved?
54
+ reserved_application_asset_paths.any? { |path| destination_file_exist?(path) }
55
+ end
56
+
57
+ def reserved_application_asset_paths
58
+ %w[
59
+ app/javascript/application.js
60
+ app/javascript/application.mjs
61
+ app/javascript/application.ts
62
+ app/javascript/application.tsx
63
+ app/assets/javascripts/application.js
64
+ app/assets/javascripts/application.js.erb
65
+ app/assets/builds/application.js
66
+ ]
67
+ end
68
+
69
+ def bulk_entrypoints_layout?
70
+ return false if existing_top_level_entrypoints.empty?
71
+
72
+ existing_top_level_entrypoints.length > 1 || existing_top_level_entrypoints.first != 'application.rb'
73
+ end
74
+
75
+ def existing_top_level_entrypoints
76
+ @existing_top_level_entrypoints ||= begin
77
+ source_root_path = destination_path(opal_source_path)
78
+ if File.directory?(source_root_path)
79
+ Dir.children(source_root_path)
80
+ .select { |entry| entry.end_with?('.rb') && File.file?(File.join(source_root_path, entry)) }
81
+ .sort
82
+ else
83
+ []
84
+ end
85
+ end
86
+ end
87
+
88
+ def ensure_gitignore_entries
89
+ return unless destination_file_exist?('.gitignore')
90
+
91
+ append_unless_present('.gitignore', "/app/assets/builds/*\n")
92
+ append_unless_present('.gitignore', "!/app/assets/builds/.keep\n")
93
+ end
94
+
95
+ def ensure_manifest_build_links
96
+ manifest_path = 'app/assets/config/manifest.js'
97
+ return unless destination_file_exist?(manifest_path)
98
+
99
+ append_unless_present(manifest_path, "//= link_directory ../builds .js\n")
100
+ append_unless_present(manifest_path, "//= link_directory ../builds .map\n")
101
+ end
102
+
103
+ def ensure_test_asset_debug
104
+ test_env_path = 'config/environments/test.rb'
105
+ return unless destination_file_exist?(test_env_path)
106
+ return unless destination_file_exist?('app/assets/config/manifest.js')
107
+
108
+ test_env_contents = File.read(destination_path(test_env_path))
109
+ return if test_env_contents.match?(/config\.assets\.debug\s*=/)
110
+
111
+ insert_into_file test_env_path, "\n config.assets.debug = true", before: /\nend\s*\z/
112
+ end
113
+
114
+ def ensure_procfile_dev
115
+ if destination_file_exist?('Procfile.dev')
116
+ append_unless_present('Procfile.dev', "opal: bin/rails opal:watch\n")
117
+ else
118
+ create_file 'Procfile.dev', "web: bin/rails server\nopal: bin/rails opal:watch\n"
119
+ end
120
+ end
121
+
122
+ def ensure_bin_dev
123
+ if destination_file_exist?('bin/dev')
124
+ # Replace bin/dev if it doesn't use Procfile.dev -- Rails 8.1+
125
+ # generates a bin/dev that just execs "bin/rails server" directly.
126
+ bin_dev_content = File.read(destination_path('bin/dev'))
127
+ return if bin_dev_content.include?('Procfile.dev')
128
+
129
+ remove_file 'bin/dev'
130
+ end
131
+
132
+ empty_directory 'bin'
133
+ template 'dev', 'bin/dev'
134
+ chmod 'bin/dev', 0o755
135
+ end
136
+
137
+ def ensure_javascript_include_tag
138
+ layout_path = 'app/views/layouts/application.html.erb'
139
+ return unless destination_file_exist?(layout_path)
140
+ return unless application_entrypoint_available?
141
+
142
+ layout_contents = File.read(destination_path(layout_path))
143
+ return if layout_contents.match?(/javascript_include_tag\s+["']#{Regexp.escape(application_logical_name)}["']/)
144
+
145
+ tag = %(<%= javascript_include_tag "#{application_logical_name}", "data-turbo-track": "reload" %>)
146
+
147
+ insert_into_file layout_path, " #{tag}\n", before: %r{\n *</head>}
148
+ end
149
+
150
+ def append_unless_present(path, contents)
151
+ append_to_file(path, contents) unless File.read(destination_path(path)).include?(contents.strip)
152
+ end
153
+
154
+ def destination_directory_exist?(relative_path)
155
+ File.directory?(destination_path(relative_path))
156
+ end
157
+
158
+ def destination_file_exist?(relative_path)
159
+ File.exist?(destination_path(relative_path))
160
+ end
161
+
162
+ def destination_path(relative_path)
163
+ File.join(destination_root, relative_path)
13
164
  end
14
165
  end
@@ -1,12 +1,12 @@
1
- require "opal"
1
+ require 'opal'
2
2
 
3
- # Uncomment the following to print out you're hello-world with Opal:
3
+ # Uncomment the following to print a hello-world message with Opal:
4
4
  #
5
- # puts "hello world!"
5
+ # puts 'hello world!'
6
6
  #
7
7
  # The following will append a hello-world to your <body> element:
8
8
  #
9
- # require "native"
9
+ # require 'native'
10
10
  # $$[:document].addEventListener :DOMContentLoaded do
11
11
  # $$[:document][:body][:innerHTML] = '<h2>Hello World!</h2>'
12
12
  # end
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env sh
2
+
3
+ if ! gem list foreman -i --silent; then
4
+ echo "Installing foreman..."
5
+ gem install foreman
6
+ fi
7
+
8
+ exec foreman start -f Procfile.dev "$@"
@@ -19,4 +19,12 @@ Rails.application.configure do
19
19
  # - :ivars # only instance variables
20
20
  #
21
21
  config.opal.assigns_in_templates = false
22
+
23
+ # Build-oriented Opal asset configuration
24
+ config.opal.source_path = Rails.root.join('<%= opal_source_path %>')
25
+ config.opal.entrypoints_path = config.opal.source_path
26
+ config.opal.build_path = Rails.root.join('app/assets/builds')
27
+ config.opal.entrypoints = <%= entrypoints_literal %>
28
+ config.opal.append_paths = []
29
+ config.opal.use_gems = []
22
30
  end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'pathname'
5
+
6
+ module Opal
7
+ module Rails
8
+ class BuilderRunner
9
+ def initialize(config:, builder_class: Opal::Builder)
10
+ @config = config
11
+ @builder_class = builder_class
12
+ end
13
+
14
+ def build(entrypoints:)
15
+ with_opal_config do
16
+ outputs = []
17
+ dependencies = {}
18
+
19
+ FileUtils.mkdir_p(build_path)
20
+
21
+ entrypoints.each_pair do |logical_name, relative_source_file|
22
+ builder = build_entrypoint(relative_source_file)
23
+ js_output = "#{logical_name}.js"
24
+
25
+ write_output(js_output, compiled_source_for(builder, js_output))
26
+ outputs << js_output
27
+
28
+ if source_map_enabled?
29
+ map_output = "#{js_output}.map"
30
+ write_output(map_output, builder.source_map.to_json)
31
+ outputs << map_output
32
+ end
33
+
34
+ dependencies[logical_name] = builder.dependent_files
35
+ end
36
+
37
+ {
38
+ outputs: outputs,
39
+ dependencies: dependencies
40
+ }
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ attr_reader :builder_class, :config
47
+
48
+ def build_entrypoint(relative_source_file)
49
+ builder = builder_class.new(
50
+ compiler_options: Opal::Config.compiler_options,
51
+ missing_require_severity: Opal::Config.missing_require_severity
52
+ )
53
+
54
+ builder.append_paths(*builder_paths)
55
+ Array(config.use_gems).each { |gem_name| builder.use_gem(gem_name) }
56
+ builder.build(relative_source_file)
57
+ builder
58
+ end
59
+
60
+ def builder_paths
61
+ [
62
+ config.entrypoints_path,
63
+ config.source_path,
64
+ *Array(config.append_paths)
65
+ ].compact.map { |path| Pathname(path).expand_path.to_s }.uniq
66
+ end
67
+
68
+ def build_path
69
+ Pathname(config.build_path).expand_path
70
+ end
71
+
72
+ def source_map_enabled?
73
+ value = config[:source_map_enabled]
74
+ value.nil? ? Opal::Config.source_map_enabled : value
75
+ end
76
+
77
+ def compiled_source_for(builder, js_output)
78
+ source = builder.to_s
79
+ return source unless source_map_enabled?
80
+
81
+ [source, "//# sourceMappingURL=#{File.basename(js_output)}.map", nil].join("\n")
82
+ end
83
+
84
+ def write_output(relative_path, contents)
85
+ output_path = build_path.join(relative_path)
86
+ FileUtils.mkdir_p(output_path.dirname)
87
+ output_path.write(contents)
88
+ end
89
+
90
+ def with_opal_config
91
+ original_config = snapshot_opal_config
92
+ apply_app_config_to_opal!
93
+ yield
94
+ ensure
95
+ restore_opal_config(original_config)
96
+ end
97
+
98
+ def snapshot_opal_config
99
+ Opal::Config.config.each_with_object({}) do |(key, value), snapshot|
100
+ snapshot[key] = begin
101
+ value.dup
102
+ rescue TypeError
103
+ value
104
+ end
105
+ end
106
+ end
107
+
108
+ def apply_app_config_to_opal!
109
+ config.each_pair do |key, value|
110
+ setter = "#{key}="
111
+ Opal::Config.public_send(setter, value) if Opal::Config.respond_to?(setter)
112
+ end
113
+ end
114
+
115
+ def restore_opal_config(snapshot)
116
+ return unless snapshot
117
+
118
+ Opal::Config.reset!
119
+ snapshot.each_pair do |key, value|
120
+ setter = "#{key}="
121
+ Opal::Config.public_send(setter, value) if Opal::Config.respond_to?(setter)
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -1,6 +1,4 @@
1
1
  require 'rails'
2
- require 'opal/sprockets'
3
- require 'sprockets/railtie'
4
2
 
5
3
  module Opal
6
4
  module Rails
@@ -11,13 +9,17 @@ module Opal
11
9
 
12
10
  config.opal.dynamic_require_severity = :ignore
13
11
  config.opal.assigns_in_templates = true
12
+ config.opal.entrypoints = { 'application' => 'application.rb' }
13
+ config.opal.append_paths = []
14
+ config.opal.use_gems = []
15
+ config.opal.suppress_legacy_upgrade_warning = false
14
16
 
15
17
  def (config.opal).assign_locals_in_templates?
16
- assigns_in_templates == true || assigns_in_templates == :locals
18
+ [true, :locals].include?(assigns_in_templates)
17
19
  end
18
20
 
19
21
  def (config.opal).assign_instance_variables_in_templates?
20
- assigns_in_templates == true || assigns_in_templates == :ivars
22
+ [true, :ivars].include?(assigns_in_templates)
21
23
  end
22
24
 
23
25
  # Cache eager_load_paths now, otherwise the assets dir is added
@@ -25,11 +27,8 @@ module Opal
25
27
  config.eager_load_paths
26
28
 
27
29
  config.before_initialize do |app|
28
- app.config.eager_load_paths = app.config.eager_load_paths.dup - Dir["#{app.root}/app/{assets,views}"]
29
- end
30
-
31
- initializer 'opal.append_assets_path', after: :append_assets_path, group: :all do |app|
32
- app.config.assets.paths.unshift(*Opal.paths)
30
+ Opal::Rails::LegacyUpgradeWarning.warn_if_needed(app)
31
+ Opal::Rails::PathSetup.apply!(app)
33
32
  end
34
33
 
35
34
  config.after_initialize do |app|
@@ -43,6 +42,9 @@ module Opal
43
42
  end
44
43
  end
45
44
 
45
+ rake_tasks do
46
+ load File.expand_path('../../tasks/opal.rake', __dir__)
47
+ end
46
48
  end
47
49
  end
48
50
  end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+
5
+ module Opal
6
+ module Rails
7
+ class EntrypointsResolver
8
+ def initialize(entrypoints_path:, entrypoints:)
9
+ @entrypoints_path = Pathname(entrypoints_path).expand_path
10
+ @entrypoints = entrypoints
11
+ end
12
+
13
+ def resolve
14
+ case entrypoints
15
+ when Hash
16
+ resolve_hash
17
+ when :all
18
+ resolve_all
19
+ else
20
+ raise InvalidEntrypointsConfigError,
21
+ "config.opal.entrypoints must be a Hash or :all, got #{entrypoints.inspect}"
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :entrypoints_path, :entrypoints
28
+
29
+ def resolve_hash
30
+ resolved = {}
31
+
32
+ unless entrypoints_path.directory?
33
+ raise MissingEntrypointError,
34
+ "Opal entrypoints_path #{entrypoints_path} does not exist"
35
+ end
36
+
37
+ entrypoints.each_pair do |logical_name, relative_source_file|
38
+ logical_name = logical_name.to_s
39
+ relative_source_file = relative_source_file.to_s
40
+ absolute_source_file = entrypoints_path.join(relative_source_file)
41
+
42
+ unless absolute_source_file.file?
43
+ raise MissingEntrypointError,
44
+ "configured Opal entrypoint #{relative_source_file.inspect} was not found under #{entrypoints_path}"
45
+ end
46
+
47
+ if resolved.key?(logical_name)
48
+ raise DuplicateEntrypointError,
49
+ "duplicate Opal logical entrypoint #{logical_name.inspect}"
50
+ end
51
+
52
+ resolved[logical_name] = relative_source_file
53
+ end
54
+
55
+ resolved
56
+ end
57
+
58
+ def resolve_all
59
+ unless entrypoints_path.directory?
60
+ raise MissingEntrypointError,
61
+ "Opal entrypoints_path #{entrypoints_path} does not exist"
62
+ end
63
+
64
+ entrypoints_path.children
65
+ .select(&:file?)
66
+ .select { |path| path.extname == '.rb' }
67
+ .sort_by { |path| path.basename.to_s }
68
+ .each_with_object({}) do |path, resolved|
69
+ logical_name = path.basename('.rb').to_s
70
+
71
+ if resolved.key?(logical_name)
72
+ raise DuplicateEntrypointError,
73
+ "duplicate Opal logical entrypoint #{logical_name.inspect}"
74
+ end
75
+
76
+ resolved[logical_name] = path.basename.to_s
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opal
4
+ module Rails
5
+ class Error < StandardError; end
6
+
7
+ class MissingEntrypointError < Error; end
8
+ class DuplicateEntrypointError < Error; end
9
+ class InvalidEntrypointsConfigError < Error; end
10
+ end
11
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+
5
+ module Opal
6
+ module Rails
7
+ class FileWatcher
8
+ def initialize(files:, extra_directories: [], &callback)
9
+ @files = normalize_paths(files)
10
+ @extra_directories = normalize_paths(extra_directories)
11
+ @callback = callback
12
+ end
13
+
14
+ def start
15
+ require 'listen'
16
+
17
+ @listener = Listen.to(*directories) do |modified, added, removed|
18
+ callback.call(
19
+ modified: normalize_paths(modified),
20
+ added: normalize_paths(added),
21
+ removed: normalize_paths(removed)
22
+ )
23
+ end
24
+ @listener.start
25
+ rescue LoadError
26
+ raise Error, 'opal:watch requires the listen gem'
27
+ end
28
+
29
+ def stop
30
+ @listener&.stop
31
+ end
32
+
33
+ private
34
+
35
+ attr_reader :callback, :extra_directories, :files
36
+
37
+ def directories
38
+ dirs = files.map { |file| File.directory?(file) ? file : File.dirname(file) }
39
+ collapse_directories((dirs + extra_directories).uniq.sort)
40
+ end
41
+
42
+ def collapse_directories(directories)
43
+ previous_dir = nil
44
+
45
+ directories.each_with_object([]) do |dir, collapsed|
46
+ next if previous_dir && dir.start_with?(previous_dir + File::SEPARATOR)
47
+
48
+ collapsed << dir
49
+ previous_dir = dir
50
+ end
51
+ end
52
+
53
+ def normalize_paths(paths)
54
+ Array(paths).map { |path| Pathname(path).expand_path.to_s }.uniq.sort
55
+ end
56
+ end
57
+ end
58
+ end
@@ -1,22 +1,22 @@
1
- require "haml"
2
- require "haml/filters"
3
- require "haml/filters/base"
1
+ require 'haml'
2
+ require 'haml/filters'
3
+ require 'haml/filters/base'
4
4
 
5
5
  module Haml
6
6
  class Filters
7
7
  class Opal < Base
8
8
  def mime_type
9
+ ::Opal::Config.esm ? 'module' : 'text/javascript'
9
10
  end
10
-
11
+
11
12
  def compile(node)
12
13
  template = [:multi]
13
14
  template << [:static, "<script type='#{mime_type}'>\n"]
14
- template << [:static, ::Opal.compile(node.value[:text]) ]
15
+ template << [:static, ::Opal.compile(node.value[:text])]
15
16
  template << [:static, "\n</script>"]
16
17
  template
17
18
  end
18
19
  end
19
- ::Opal::Config.esm ? 'module' : 'text/javascript'
20
20
  end
21
21
  end
22
22
 
@@ -1,9 +1,6 @@
1
+ require 'opal'
1
2
  require 'haml'
2
3
 
3
- haml_version = Haml::VERSION.to_i
4
+ raise LoadError, 'opal-rails requires Haml 6 or newer for the :opal filter' if Haml::VERSION.to_i < 6
4
5
 
5
- if haml_version < 6
6
- require 'opal/rails/haml5_filter'
7
- else
8
- require 'opal/rails/haml6_filter'
9
- end
6
+ require 'opal/rails/haml6_filter'
@@ -0,0 +1,95 @@
1
+ module Opal
2
+ module Rails
3
+ module LegacyUpgradeWarning
4
+ module_function
5
+
6
+ LEGACY_OPAL_CONFIG_KEYS = %w[
7
+ method_missing_enabled
8
+ const_missing_enabled
9
+ arity_check_enabled
10
+ freezing_stubs_enabled
11
+ missing_require_severity
12
+ ].freeze
13
+
14
+ BUILD_CONFIG_KEYS = %w[
15
+ source_path
16
+ entrypoints_path
17
+ build_path
18
+ entrypoints
19
+ ].freeze
20
+
21
+ def warn_if_needed(app, output: $stderr)
22
+ return false if suppress_warning?(app)
23
+
24
+ warning = warning_for(app.root)
25
+ return false unless warning
26
+
27
+ output.puts(warning)
28
+ true
29
+ end
30
+
31
+ def warning_for(root)
32
+ signals = legacy_signals(root)
33
+ return if signals.empty?
34
+
35
+ <<~WARNING
36
+ WARNING: opal-rails detected a likely 2.x application layout that has not been ported to the 3.x build pipeline yet.
37
+
38
+ This app may fail after upgrade with missing `app/opal` entrypoints or missing `application.js` assets.
39
+ Pin `opal-rails` to the 2.0 series until the app can be ported, or follow `PORTING.md` before continuing.
40
+ If you have already ported the app and want to silence this check, set `config.opal.suppress_legacy_upgrade_warning = true`.
41
+
42
+ Detected legacy signals:
43
+ #{signals.map { |signal| "- #{signal}" }.join("\n")}
44
+ WARNING
45
+ end
46
+
47
+ def suppress_warning?(app)
48
+ return false unless app.respond_to?(:config)
49
+
50
+ app.config.respond_to?(:opal) && app.config.opal.suppress_legacy_upgrade_warning
51
+ end
52
+
53
+ def legacy_signals(root)
54
+ root = Pathname(root)
55
+ signals = []
56
+
57
+ initializer = root.join('config/initializers/opal.rb')
58
+ initializer_body = initializer.exist? ? initializer.read : nil
59
+
60
+ if legacy_initializer?(initializer_body)
61
+ signals << 'config/initializers/opal.rb still uses legacy 2.x runtime settings'
62
+ end
63
+
64
+ legacy_entrypoints(root).each do |entrypoint|
65
+ signals << "legacy Opal asset entrypoint present at #{relative_to_root(entrypoint, root)}"
66
+ end
67
+
68
+ manifest = root.join('app/assets/config/manifest.js')
69
+ if manifest.exist? && manifest.read.match?(%r{link_(?:tree|directory)\s+\.\./javascript\b})
70
+ signals << 'app/assets/config/manifest.js still links the legacy javascript asset tree'
71
+ end
72
+
73
+ return [] unless initializer_body && signals.length >= 2
74
+ return signals if root.join('app/opal').directory?
75
+
76
+ signals << 'expected 3.x source root app/opal is missing'
77
+ end
78
+
79
+ def legacy_initializer?(initializer_body)
80
+ return false unless initializer_body
81
+ return false if BUILD_CONFIG_KEYS.any? { |key| initializer_body.include?("config.opal.#{key}") }
82
+
83
+ LEGACY_OPAL_CONFIG_KEYS.any? { |key| initializer_body.include?("config.opal.#{key}") }
84
+ end
85
+
86
+ def legacy_entrypoints(root)
87
+ Dir[root.join('app/assets/{javascript,javascripts}/**/*.js.rb').to_s].sort.map { |path| Pathname(path) }
88
+ end
89
+
90
+ def relative_to_root(path, root)
91
+ path.relative_path_from(root).to_s
92
+ end
93
+ end
94
+ end
95
+ end