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
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'fileutils'
5
+ require 'pathname'
6
+ require 'tempfile'
7
+
8
+ module Opal
9
+ module Rails
10
+ class OutputsManifest
11
+ FILE_NAME = '.opal-build-manifest.json'
12
+
13
+ def initialize(build_path:)
14
+ @build_path = Pathname(build_path).expand_path
15
+ end
16
+
17
+ def read_outputs
18
+ return [] unless manifest_path.exist?
19
+
20
+ JSON.parse(manifest_path.read).fetch('outputs')
21
+ rescue JSON::ParserError, KeyError
22
+ nil
23
+ end
24
+
25
+ def prune_stale!(current_outputs)
26
+ previous_outputs = read_outputs
27
+ return false if previous_outputs.nil?
28
+
29
+ stale_outputs(previous_outputs, current_outputs).each do |relative_path|
30
+ full_path = safe_build_path(relative_path)
31
+ full_path.delete if full_path&.exist?
32
+ end
33
+
34
+ true
35
+ end
36
+
37
+ def write!(outputs)
38
+ FileUtils.mkdir_p(build_path)
39
+
40
+ Tempfile.create(['opal-build-manifest', '.json'], build_path.to_s) do |file|
41
+ file.write(JSON.pretty_generate('version' => 1, 'outputs' => outputs.sort))
42
+ file.flush
43
+ FileUtils.mv(file.path, manifest_path)
44
+ end
45
+ end
46
+
47
+ def clobber!
48
+ outputs = read_outputs
49
+ return nil if outputs.nil?
50
+
51
+ outputs.each do |relative_path|
52
+ full_path = safe_build_path(relative_path)
53
+ full_path.delete if full_path&.exist?
54
+ end
55
+
56
+ manifest_path.delete if manifest_path.exist?
57
+
58
+ outputs
59
+ end
60
+
61
+ private
62
+
63
+ attr_reader :build_path
64
+
65
+ def manifest_path
66
+ build_path.join(FILE_NAME)
67
+ end
68
+
69
+ def stale_outputs(previous_outputs, current_outputs)
70
+ previous_outputs - current_outputs
71
+ end
72
+
73
+ def safe_build_path(relative_path)
74
+ full_path = build_path.join(relative_path).expand_path
75
+ return full_path if full_path.to_s.start_with?(build_path.to_s + File::SEPARATOR) ||
76
+ full_path.to_s == build_path.to_s
77
+
78
+ nil
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+
5
+ module Opal
6
+ module Rails
7
+ module PathSetup
8
+ module_function
9
+
10
+ def apply!(app, autoloaders: ::Rails.autoloaders)
11
+ app.config.opal.source_path ||= app.root.join('app/opal')
12
+ app.config.opal.entrypoints_path ||= app.config.opal.source_path
13
+ app.config.opal.build_path ||= app.root.join('app/assets/builds')
14
+
15
+ source_path = Pathname(app.config.opal.source_path).expand_path
16
+ app_assets_path = app.root.join('app/assets').expand_path
17
+
18
+ app.config.eager_load_paths = app.config.eager_load_paths.dup - Dir["#{app.root}/app/{assets,views}"]
19
+
20
+ app.config.eager_load_paths -= [source_path.to_s] if source_path.to_s.start_with?(app.root.join('app').to_s)
21
+
22
+ return if source_path.to_s.start_with?(app_assets_path.to_s)
23
+
24
+ Array(autoloaders).each { |autoloader| autoloader.ignore(source_path) }
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rake'
4
+
5
+ module Opal
6
+ module Rails
7
+ module TaskHooks
8
+ module_function
9
+
10
+ BUILD_HOOKS = [
11
+ 'assets:precompile',
12
+ 'test:prepare',
13
+ 'spec:prepare'
14
+ ].freeze
15
+
16
+ # Ensure `rake test` invokes test:prepare (which Rails defines but
17
+ # does not wire as a prerequisite of the :test task).
18
+ PREPARE_HOOKS = {
19
+ 'test' => 'test:prepare',
20
+ 'spec' => 'spec:prepare'
21
+ }.freeze
22
+
23
+ CLOBBER_HOOKS = [
24
+ 'assets:clobber'
25
+ ].freeze
26
+
27
+ def apply!(task_manager: Rake::Task)
28
+ BUILD_HOOKS.each do |task_name|
29
+ attach(task_name, prerequisite: 'opal:build', task_manager: task_manager)
30
+ end
31
+ CLOBBER_HOOKS.each do |task_name|
32
+ attach(task_name, prerequisite: 'opal:clobber', task_manager: task_manager)
33
+ end
34
+ PREPARE_HOOKS.each do |task_name, prerequisite|
35
+ attach(task_name, prerequisite: prerequisite, task_manager: task_manager)
36
+ end
37
+ end
38
+
39
+ def attach(task_name, prerequisite:, task_manager: Rake::Task)
40
+ return unless task_manager.task_defined?(task_name)
41
+
42
+ task = task_manager[task_name]
43
+ return if task.prerequisites.include?(prerequisite)
44
+
45
+ task.enhance([prerequisite])
46
+ end
47
+ end
48
+ end
49
+ end
@@ -1,5 +1,5 @@
1
1
  module Opal
2
2
  module Rails
3
- VERSION = '2.0.3'
3
+ VERSION = '3.0.0'
4
4
  end
5
5
  end
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+
5
+ module Opal
6
+ module Rails
7
+ class WatchRunner
8
+ def initialize(config:, resolver: nil, builder_runner: nil, manifest: nil, file_watcher_class: FileWatcher,
9
+ output: $stdout, error_output: $stderr, kernel: Kernel)
10
+ @config = config
11
+ @resolver = resolver
12
+ @builder_runner = builder_runner
13
+ @manifest = manifest
14
+ @file_watcher_class = file_watcher_class
15
+ @output = output
16
+ @error_output = error_output
17
+ @kernel = kernel
18
+ @dependencies_by_entrypoint = {}
19
+ @outputs_by_entrypoint = {}
20
+ @reverse_dependencies = Hash.new { |hash, key| hash[key] = [] }
21
+ @opal_dependencies = []
22
+ end
23
+
24
+ def watch
25
+ start!
26
+ output.puts '* Opal watcher started'
27
+ kernel.sleep
28
+ rescue Interrupt
29
+ output.puts '* Stopping Opal watcher...'
30
+ ensure
31
+ watcher&.stop
32
+ end
33
+
34
+ def start!
35
+ rebuild_all!
36
+ end
37
+
38
+ def process_changes(modified:, added:, removed:)
39
+ modified = normalize_paths(modified)
40
+ added = normalize_paths(added)
41
+ removed = normalize_paths(removed)
42
+ changed = (modified + added + removed).uniq.sort
43
+ return if changed.empty?
44
+
45
+ if full_rebuild_required?(changed: changed, modified: modified, added: added, removed: removed)
46
+ error_output.puts "* Modified code: #{changed.join(', ')}; rebuilding all entrypoints"
47
+ rebuild_all!
48
+ else
49
+ logical_names = modified.flat_map { |path| reverse_dependencies[path] }.uniq.sort
50
+ return if logical_names.empty?
51
+
52
+ error_output.puts "* Modified code: #{modified.join(', ')}; rebuilding #{logical_names.join(', ')}"
53
+ rebuild_entrypoints!(logical_names)
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ attr_reader :builder_runner, :config, :error_output, :file_watcher_class, :kernel, :manifest, :opal_dependencies,
60
+ :output, :resolved_entrypoints, :resolver, :reverse_dependencies
61
+ attr_accessor :watcher
62
+
63
+ def rebuild_all!
64
+ @resolved_entrypoints = current_resolver.resolve
65
+ result = current_builder_runner.build(entrypoints: resolved_entrypoints)
66
+
67
+ @dependencies_by_entrypoint = normalize_dependency_map(result.fetch(:dependencies))
68
+ @outputs_by_entrypoint = resolved_entrypoints.each_with_object({}) do |(logical_name, _), outputs|
69
+ outputs[logical_name] = outputs_for(logical_name)
70
+ end
71
+
72
+ current_manifest.prune_stale!(owned_outputs)
73
+ current_manifest.write!(owned_outputs)
74
+ refresh_watcher!
75
+ rescue StandardError => e
76
+ error_output.puts "* Opal build error: #{e.message}"
77
+ refresh_watcher! if @resolved_entrypoints
78
+ end
79
+
80
+ def rebuild_entrypoints!(logical_names)
81
+ entrypoints = resolved_entrypoints.slice(*logical_names)
82
+ result = current_builder_runner.build(entrypoints: entrypoints)
83
+
84
+ normalize_dependency_map(result.fetch(:dependencies)).each_pair do |logical_name, files|
85
+ @dependencies_by_entrypoint[logical_name] = files
86
+ end
87
+
88
+ current_manifest.write!(owned_outputs)
89
+ refresh_watcher!
90
+ rescue StandardError => e
91
+ error_output.puts "* Opal build error: #{e.message}"
92
+ end
93
+
94
+ def refresh_watcher!
95
+ @opal_dependencies = normalize_paths(Opal.dependent_files)
96
+ rebuild_reverse_dependencies!
97
+
98
+ watcher&.stop
99
+ self.watcher = file_watcher_class.new(files: watched_files,
100
+ extra_directories: extra_directories) do |modified:, added:, removed:|
101
+ process_changes(modified: modified, added: added, removed: removed)
102
+ end
103
+ watcher.start
104
+ end
105
+
106
+ def rebuild_reverse_dependencies!
107
+ @reverse_dependencies = Hash.new { |hash, key| hash[key] = [] }
108
+
109
+ @dependencies_by_entrypoint.each_pair do |logical_name, files|
110
+ files.each { |path| @reverse_dependencies[path] << logical_name }
111
+ end
112
+ end
113
+
114
+ def watched_files
115
+ (opal_dependencies + @dependencies_by_entrypoint.values.flatten).uniq.sort
116
+ end
117
+
118
+ def owned_outputs
119
+ @outputs_by_entrypoint.values.flatten.uniq.sort
120
+ end
121
+
122
+ def outputs_for(logical_name)
123
+ outputs = ["#{logical_name}.js"]
124
+ outputs << "#{logical_name}.js.map" if source_map_enabled?
125
+ outputs
126
+ end
127
+
128
+ def full_rebuild_required?(changed:, modified:, added:, removed:)
129
+ return true unless (changed & opal_dependencies).empty?
130
+ return true unless added.empty? && removed.empty?
131
+ return true if modified.any? { |path| reverse_dependencies[path].empty? }
132
+
133
+ false
134
+ end
135
+
136
+ def source_map_enabled?
137
+ value = config[:source_map_enabled]
138
+ value.nil? ? Opal::Config.source_map_enabled : value
139
+ end
140
+
141
+ def extra_directories
142
+ directories = [config.source_path, *Array(config.append_paths)]
143
+ directories << config.entrypoints_path if config.entrypoints == :all
144
+ normalize_paths(directories)
145
+ end
146
+
147
+ def current_resolver
148
+ @resolver ||= EntrypointsResolver.new(
149
+ entrypoints_path: config.entrypoints_path,
150
+ entrypoints: config.entrypoints
151
+ )
152
+ end
153
+
154
+ def current_builder_runner
155
+ @builder_runner ||= BuilderRunner.new(config: config)
156
+ end
157
+
158
+ def current_manifest
159
+ @manifest ||= OutputsManifest.new(build_path: config.build_path)
160
+ end
161
+
162
+ def normalize_dependency_map(dependencies)
163
+ dependencies.each_with_object({}) do |(logical_name, files), normalized|
164
+ normalized[logical_name] = normalize_paths(files)
165
+ end
166
+ end
167
+
168
+ def normalize_paths(paths)
169
+ Array(paths).map { |path| Pathname(path).expand_path.to_s }.uniq.sort
170
+ end
171
+ end
172
+ end
173
+ end
data/lib/opal/rails.rb CHANGED
@@ -1,5 +1,12 @@
1
1
  require 'opal'
2
2
 
3
+ require 'opal/rails/errors'
4
+ require 'opal/rails/entrypoints_resolver'
5
+ require 'opal/rails/builder_runner'
6
+ require 'opal/rails/outputs_manifest'
7
+ require 'opal/rails/legacy_upgrade_warning'
8
+ require 'opal/rails/path_setup'
9
+ require 'opal/rails/task_hooks'
3
10
  require 'opal/rails/engine'
4
11
  require 'opal/rails/template_handler'
5
12
  require 'opal/rails/version'
@@ -0,0 +1,48 @@
1
+ namespace :opal do
2
+ desc 'Build configured Opal entrypoints into app/assets/builds'
3
+ task build: :environment do
4
+ app_config = Rails.application.config.opal
5
+
6
+ resolver = Opal::Rails::EntrypointsResolver.new(
7
+ entrypoints_path: app_config.entrypoints_path,
8
+ entrypoints: app_config.entrypoints
9
+ )
10
+ entrypoints = resolver.resolve
11
+
12
+ runner = Opal::Rails::BuilderRunner.new(config: app_config)
13
+ result = runner.build(entrypoints: entrypoints)
14
+
15
+ manifest = Opal::Rails::OutputsManifest.new(build_path: app_config.build_path)
16
+ manifest.prune_stale!(result[:outputs])
17
+ manifest.write!(result[:outputs])
18
+
19
+ if result[:outputs].empty?
20
+ puts 'Built 0 Opal assets'
21
+ else
22
+ puts "Built Opal assets: #{result[:outputs].join(', ')}"
23
+ end
24
+ end
25
+
26
+ desc 'Remove Opal-owned build outputs from app/assets/builds'
27
+ task clobber: :environment do
28
+ manifest = Opal::Rails::OutputsManifest.new(build_path: Rails.application.config.opal.build_path)
29
+ removed_outputs = manifest.clobber!
30
+
31
+ if removed_outputs.nil?
32
+ warn 'Skipped Opal clobber because the build manifest is unreadable'
33
+ elsif removed_outputs.empty?
34
+ puts 'Removed 0 Opal assets'
35
+ else
36
+ puts "Removed Opal assets: #{removed_outputs.join(', ')}"
37
+ end
38
+ end
39
+
40
+ desc 'Watch configured Opal entrypoints and rebuild on change'
41
+ task watch: :environment do
42
+ require 'opal/rails/file_watcher'
43
+ require 'opal/rails/watch_runner'
44
+ Opal::Rails::WatchRunner.new(config: Rails.application.config.opal).watch
45
+ end
46
+ end
47
+
48
+ Opal::Rails::TaskHooks.apply!
data/opal-rails.gemspec CHANGED
@@ -8,8 +8,8 @@ Gem::Specification.new do |spec|
8
8
  spec.authors = ['Elia Schito']
9
9
  spec.email = ['elia@schito.me']
10
10
 
11
- spec.summary = %q{Rails bindings for opal JS engine}
12
- spec.description = %q{Rails bindings for opal JS engine}
11
+ spec.summary = 'Rails bindings for opal JS engine'
12
+ spec.description = 'Rails bindings for opal JS engine'
13
13
  spec.homepage = 'https://github.com/opal/opal-rails#readme'
14
14
  spec.license = 'MIT-LICENSE'
15
15
 
@@ -17,7 +17,7 @@ Gem::Specification.new do |spec|
17
17
  spec.metadata['source_code_uri'] = 'https://github.com/opal/opal-rails#readme'
18
18
  spec.metadata['changelog_uri'] = 'https://github.com/opal/opal-rails/blob/master/CHANGELOG.md'
19
19
 
20
- spec.required_ruby_version = Gem::Requirement.new('>= 2.5')
20
+ spec.required_ruby_version = Gem::Requirement.new('>= 2.7')
21
21
 
22
22
  # Specify which files should be added to the gem when it is released.
23
23
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
@@ -25,24 +25,33 @@ Gem::Specification.new do |spec|
25
25
 
26
26
  spec.files = files.grep_v(%r{^(test|spec|features)/})
27
27
  spec.test_files = files.grep(%r{^(test|spec|features)/})
28
- spec.bindir = "exe"
28
+ spec.bindir = 'exe'
29
29
  spec.executables = files.grep(%r{^exe/}) { |f| File.basename(f) }
30
- spec.require_paths = ["lib"]
30
+ spec.require_paths = ['lib']
31
31
 
32
- spec.add_dependency 'rails', '>= 6.0', '< 7.1'
33
- spec.add_dependency 'sprockets-rails', '>= 3.0'
32
+ spec.add_dependency 'rails', '>= 7.0', '< 8.2'
34
33
 
35
- spec.add_dependency 'opal', '~> 1.0'
36
- spec.add_dependency 'opal-sprockets', '~> 1.0'
34
+ spec.add_dependency 'opal', '>= 1.7.0'
37
35
 
38
- spec.add_development_dependency 'haml'
36
+ spec.add_dependency 'listen', '>= 3.0'
39
37
 
40
- spec.add_development_dependency 'execjs'
41
- spec.add_development_dependency 'launchy'
38
+ spec.add_development_dependency 'haml', '>= 6'
39
+
40
+ spec.add_development_dependency 'appraisal', '~> 2.1'
41
+ spec.add_development_dependency 'base64'
42
+ spec.add_development_dependency 'benchmark'
43
+ spec.add_development_dependency 'bigdecimal'
42
44
  spec.add_development_dependency 'capybara', '~> 3.25'
45
+ spec.add_development_dependency 'cgi'
43
46
  spec.add_development_dependency 'cuprite'
47
+ spec.add_development_dependency 'execjs'
48
+ spec.add_development_dependency 'launchy'
49
+ spec.add_development_dependency 'logger'
50
+ spec.add_development_dependency 'mutex_m'
51
+ spec.add_development_dependency 'ostruct'
52
+ spec.add_development_dependency 'puma'
44
53
  spec.add_development_dependency 'rspec-rails'
45
- spec.add_development_dependency 'appraisal', '~> 2.1'
54
+ spec.add_development_dependency 'sprockets-rails', '>= 3.0'
46
55
  spec.add_development_dependency 'sqlite3'
47
- spec.add_development_dependency 'puma'
56
+ spec.add_development_dependency 'tsort'
48
57
  end