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,116 @@
1
+ require 'spec_helper'
2
+ require 'rake'
3
+ require 'tmpdir'
4
+ require 'fileutils'
5
+
6
+ RSpec.describe 'opal:build task' do
7
+ before(:all) do
8
+ Rails.application.load_tasks unless Rake::Task.task_defined?('opal:build')
9
+ end
10
+
11
+ around do |example|
12
+ Dir.mktmpdir do |dir|
13
+ root = Pathname(dir)
14
+ source_path = root.join('app/opal')
15
+ build_path = root.join('app/assets/builds')
16
+ FileUtils.mkdir_p(source_path)
17
+ FileUtils.mkdir_p(build_path)
18
+
19
+ source_path.join('application.rb').write("require 'opal'\nputs 'built from task'\n")
20
+
21
+ config = Rails.application.config.opal
22
+ original_source_path = config.source_path
23
+ original_entrypoints_path = config.entrypoints_path
24
+ original_build_path = config.build_path
25
+ original_entrypoints = config.entrypoints
26
+ original_append_paths = config.append_paths
27
+ original_use_gems = config.use_gems
28
+ original_source_map_enabled = config.source_map_enabled if config.respond_to?(:source_map_enabled)
29
+
30
+ config.source_path = source_path
31
+ config.entrypoints_path = source_path
32
+ config.build_path = build_path
33
+ config.entrypoints = { 'application' => 'application.rb' }
34
+ config.append_paths = []
35
+ config.use_gems = []
36
+ config.source_map_enabled = true
37
+
38
+ example.run
39
+ ensure
40
+ config.source_path = original_source_path
41
+ config.entrypoints_path = original_entrypoints_path
42
+ config.build_path = original_build_path
43
+ config.entrypoints = original_entrypoints
44
+ config.append_paths = original_append_paths
45
+ config.use_gems = original_use_gems
46
+ config.source_map_enabled = original_source_map_enabled if config.respond_to?(:source_map_enabled)
47
+ end
48
+ end
49
+
50
+ it 'builds assets and writes an ownership manifest' do
51
+ Rake::Task['opal:build'].reenable
52
+ Rake::Task['opal:build'].invoke
53
+
54
+ build_path = Pathname(Rails.application.config.opal.build_path)
55
+
56
+ expect(build_path.join('application.js')).to exist
57
+ expect(build_path.join('application.js.map')).to exist
58
+ expect(build_path.join('.opal-build-manifest.json').read).to include('application.js')
59
+ end
60
+
61
+ it 'builds and prunes bulk-discovered entrypoints in :all mode' do
62
+ Dir.mktmpdir do |dir|
63
+ root = Pathname(dir)
64
+ source_path = root.join('app/assets/opal')
65
+ build_path = root.join('app/assets/builds')
66
+ FileUtils.mkdir_p(source_path.join('nested'))
67
+ FileUtils.mkdir_p(build_path)
68
+
69
+ source_path.join('application.rb').write("require 'opal'\nputs 'app entrypoint'\n")
70
+ source_path.join('dashboard.rb').write("require 'opal'\nputs 'dashboard entrypoint'\n")
71
+ source_path.join('nested/ignored.rb').write("require 'opal'\nputs 'ignore me'\n")
72
+
73
+ config = Rails.application.config.opal
74
+ original_source_path = config.source_path
75
+ original_entrypoints_path = config.entrypoints_path
76
+ original_build_path = config.build_path
77
+ original_entrypoints = config.entrypoints
78
+ original_append_paths = config.append_paths
79
+ original_use_gems = config.use_gems
80
+ original_source_map_enabled = config.source_map_enabled if config.respond_to?(:source_map_enabled)
81
+
82
+ config.source_path = source_path
83
+ config.entrypoints_path = source_path
84
+ config.build_path = build_path
85
+ config.entrypoints = :all
86
+ config.append_paths = []
87
+ config.use_gems = []
88
+ config.source_map_enabled = true
89
+
90
+ Rake::Task['opal:build'].reenable
91
+ Rake::Task['opal:build'].invoke
92
+
93
+ expect(build_path.join('application.js')).to exist
94
+ expect(build_path.join('dashboard.js')).to exist
95
+ expect(build_path.join('nested.js')).not_to exist
96
+ expect(build_path.join('.opal-build-manifest.json').read).to include('dashboard.js')
97
+
98
+ source_path.join('dashboard.rb').delete
99
+
100
+ Rake::Task['opal:build'].reenable
101
+ Rake::Task['opal:build'].invoke
102
+
103
+ expect(build_path.join('application.js')).to exist
104
+ expect(build_path.join('dashboard.js')).not_to exist
105
+ expect(build_path.join('.opal-build-manifest.json').read).not_to include('dashboard.js')
106
+ ensure
107
+ config.source_path = original_source_path
108
+ config.entrypoints_path = original_entrypoints_path
109
+ config.build_path = original_build_path
110
+ config.entrypoints = original_entrypoints
111
+ config.append_paths = original_append_paths
112
+ config.use_gems = original_use_gems
113
+ config.source_map_enabled = original_source_map_enabled if config.respond_to?(:source_map_enabled)
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,151 @@
1
+ require 'spec_helper'
2
+ require 'tmpdir'
3
+ require 'fileutils'
4
+
5
+ RSpec.describe Opal::Rails::BuilderRunner do
6
+ around do |example|
7
+ Dir.mktmpdir do |dir|
8
+ @root = Pathname(dir)
9
+ @source_path = @root.join('app/opal')
10
+ @entrypoints_path = @root.join('app/opal/entrypoints')
11
+ @build_path = @root.join('app/assets/builds')
12
+ @shared_path = @root.join('app/shared/opal')
13
+
14
+ FileUtils.mkdir_p(@source_path)
15
+ FileUtils.mkdir_p(@entrypoints_path)
16
+ FileUtils.mkdir_p(@build_path)
17
+ FileUtils.mkdir_p(@shared_path)
18
+
19
+ example.run
20
+ end
21
+ end
22
+
23
+ it 'builds configured entrypoints and writes source maps' do
24
+ @source_path.join('message.rb').write("module Message\n def self.text\n 'hello from opal'\n end\nend\n")
25
+ @entrypoints_path.join('application.rb').write("require 'opal'\nrequire 'message'\nputs Message.text\n")
26
+
27
+ config = ActiveSupport::OrderedOptions.new
28
+ config.source_path = @source_path
29
+ config.entrypoints_path = @entrypoints_path
30
+ config.build_path = @build_path
31
+ config.append_paths = []
32
+ config.use_gems = []
33
+ config.source_map_enabled = true
34
+
35
+ result = described_class.new(config: config).build(
36
+ entrypoints: { 'application' => 'application.rb' }
37
+ )
38
+
39
+ expect(result[:outputs]).to contain_exactly('application.js', 'application.js.map')
40
+ expect(@build_path.join('application.js').read).to include('sourceMappingURL=application.js.map')
41
+ expect(@build_path.join('application.js').read).to include('hello from opal')
42
+ expect(@build_path.join('application.js.map').read).to include('"version"')
43
+ expect(result[:dependencies]['application']).to include(@entrypoints_path.join('application.rb').to_s)
44
+ expect(result[:dependencies]['application']).to include(@source_path.join('message.rb').to_s)
45
+ end
46
+
47
+ it 'builds the file from entrypoints_path when source roots share a basename' do
48
+ @source_path.join('application.rb').write("puts 'support file'\n")
49
+ @entrypoints_path.join('application.rb').write("require 'opal'\nputs 'entrypoint file'\n")
50
+
51
+ config = ActiveSupport::OrderedOptions.new
52
+ config.source_path = @source_path
53
+ config.entrypoints_path = @entrypoints_path
54
+ config.build_path = @build_path
55
+ config.append_paths = []
56
+ config.use_gems = []
57
+ config.source_map_enabled = false
58
+
59
+ described_class.new(config: config).build(
60
+ entrypoints: { 'application' => 'application.rb' }
61
+ )
62
+
63
+ built_source = @build_path.join('application.js').read
64
+ expect(built_source).to include('entrypoint file')
65
+ expect(built_source).not_to include('support file')
66
+ end
67
+
68
+ it 'resolves requires from configured append_paths' do
69
+ @shared_path.join('shared_message.rb').write("module SharedMessage\n def self.text\n 'hello from append paths'\n end\nend\n")
70
+ @entrypoints_path.join('application.rb').write("require 'opal'\nrequire 'shared_message'\nputs SharedMessage.text\n")
71
+
72
+ config = ActiveSupport::OrderedOptions.new
73
+ config.source_path = @source_path
74
+ config.entrypoints_path = @entrypoints_path
75
+ config.build_path = @build_path
76
+ config.append_paths = [@shared_path]
77
+ config.use_gems = []
78
+ config.source_map_enabled = true
79
+
80
+ result = described_class.new(config: config).build(
81
+ entrypoints: { 'application' => 'application.rb' }
82
+ )
83
+
84
+ expect(@build_path.join('application.js').read).to include('hello from append paths')
85
+ expect(result[:dependencies]['application']).to include(@shared_path.join('shared_message.rb').to_s)
86
+ end
87
+
88
+ it 'forwards append_paths and use_gems to the builder' do
89
+ fake_builder_class = Class.new do
90
+ class << self
91
+ attr_accessor :instances
92
+ end
93
+
94
+ self.instances = []
95
+
96
+ attr_reader :appended_paths, :built_files, :used_gems
97
+
98
+ def initialize(**)
99
+ @appended_paths = []
100
+ @built_files = []
101
+ @used_gems = []
102
+ self.class.instances << self
103
+ end
104
+
105
+ def append_paths(*paths)
106
+ appended_paths.concat(paths)
107
+ end
108
+
109
+ def use_gem(gem_name)
110
+ used_gems << gem_name
111
+ end
112
+
113
+ def build(relative_source_file)
114
+ built_files << relative_source_file
115
+ end
116
+
117
+ def to_s
118
+ '// fake builder output'
119
+ end
120
+
121
+ def source_map
122
+ Struct.new(:to_json).new('{"version":3}')
123
+ end
124
+
125
+ def dependent_files
126
+ []
127
+ end
128
+ end
129
+
130
+ @entrypoints_path.join('application.rb').write("puts 'noop'\n")
131
+
132
+ config = ActiveSupport::OrderedOptions.new
133
+ config.source_path = @source_path
134
+ config.entrypoints_path = @entrypoints_path
135
+ config.build_path = @build_path
136
+ config.append_paths = [@shared_path]
137
+ config.use_gems = %w[cannonbol browser]
138
+ config.source_map_enabled = false
139
+
140
+ described_class.new(config: config, builder_class: fake_builder_class).build(
141
+ entrypoints: { 'application' => 'application.rb' }
142
+ )
143
+
144
+ fake_builder = fake_builder_class.instances.fetch(0)
145
+ expect(fake_builder.appended_paths).to include(@source_path.expand_path.to_s)
146
+ expect(fake_builder.appended_paths).to include(@entrypoints_path.expand_path.to_s)
147
+ expect(fake_builder.appended_paths).to include(@shared_path.expand_path.to_s)
148
+ expect(fake_builder.used_gems).to eq(%w[cannonbol browser])
149
+ expect(fake_builder.built_files).to eq(['application.rb'])
150
+ end
151
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rake'
4
+
5
+ RSpec.describe 'opal:clobber task' do
6
+ let(:tasks_path) { File.expand_path('../../../../lib/tasks/opal.rake', __dir__) }
7
+
8
+ before do
9
+ Rake.application = Rake::Application.new
10
+ Rails.application.load_tasks unless Rake::Task.task_defined?('opal:clobber')
11
+ load tasks_path unless Rake::Task.task_defined?('opal:clobber')
12
+ end
13
+
14
+ after do
15
+ Rake.application = nil
16
+ end
17
+
18
+ it 'removes only Opal-owned outputs and leaves unrelated files alone' do
19
+ Dir.mktmpdir do |tmpdir|
20
+ app_root = Pathname(tmpdir)
21
+ build_path = app_root.join('app/assets/builds')
22
+ FileUtils.mkdir_p(build_path)
23
+
24
+ application_js = build_path.join('application.js')
25
+ application_map = build_path.join('application.js.map')
26
+ unrelated = build_path.join('application.css')
27
+ keep_file = build_path.join('.keep')
28
+
29
+ application_js.write('// opal output')
30
+ application_map.write('{"version":3}')
31
+ unrelated.write('body {}')
32
+ keep_file.write('')
33
+
34
+ manifest = Opal::Rails::OutputsManifest.new(build_path: build_path)
35
+ manifest.write!(%w[application.js application.js.map])
36
+
37
+ original_build_path = Rails.application.config.opal.build_path
38
+ Rails.application.config.opal.build_path = build_path
39
+
40
+ begin
41
+ task = Rake::Task['opal:clobber']
42
+ task.reenable
43
+ task.invoke
44
+
45
+ expect(application_js).not_to exist
46
+ expect(application_map).not_to exist
47
+ expect(build_path.join(Opal::Rails::OutputsManifest::FILE_NAME)).not_to exist
48
+ expect(unrelated).to exist
49
+ expect(keep_file).to exist
50
+ ensure
51
+ Rails.application.config.opal.build_path = original_build_path
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,50 @@
1
+ require 'spec_helper'
2
+ require 'tmpdir'
3
+ require 'fileutils'
4
+
5
+ RSpec.describe Opal::Rails::EntrypointsResolver do
6
+ around do |example|
7
+ Dir.mktmpdir do |dir|
8
+ @entrypoints_path = Pathname(dir)
9
+ example.run
10
+ end
11
+ end
12
+
13
+ it 'resolves an explicit mapping' do
14
+ @entrypoints_path.join('application.rb').write("puts 'hello'\n")
15
+
16
+ resolver = described_class.new(
17
+ entrypoints_path: @entrypoints_path,
18
+ entrypoints: { 'application' => 'application.rb' }
19
+ )
20
+
21
+ expect(resolver.resolve).to eq('application' => 'application.rb')
22
+ end
23
+
24
+ it 'rejects missing explicit files' do
25
+ resolver = described_class.new(
26
+ entrypoints_path: @entrypoints_path,
27
+ entrypoints: { 'application' => 'application.rb' }
28
+ )
29
+
30
+ expect { resolver.resolve }
31
+ .to raise_error(Opal::Rails::MissingEntrypointError, /application\.rb/)
32
+ end
33
+
34
+ it 'resolves top-level ruby files when entrypoints is :all' do
35
+ @entrypoints_path.join('zeta.rb').write("puts 'zeta'\n")
36
+ @entrypoints_path.join('alpha.rb').write("puts 'alpha'\n")
37
+ FileUtils.mkdir_p(@entrypoints_path.join('nested'))
38
+ @entrypoints_path.join('nested/ignored.rb').write("puts 'ignored'\n")
39
+
40
+ resolver = described_class.new(
41
+ entrypoints_path: @entrypoints_path,
42
+ entrypoints: :all
43
+ )
44
+
45
+ expect(resolver.resolve).to eq(
46
+ 'alpha' => 'alpha.rb',
47
+ 'zeta' => 'zeta.rb'
48
+ )
49
+ end
50
+ end
@@ -0,0 +1,41 @@
1
+ require 'open3'
2
+ require 'rbconfig'
3
+ require 'opal/rails/haml_filter'
4
+
5
+ RSpec.describe 'Opal Haml filter loader' do
6
+ let(:project_root) { File.expand_path('../../..', __dir__) }
7
+
8
+ def run_loader_with_version(version)
9
+ script = <<~RUBY
10
+ require 'haml'
11
+ Haml.send(:remove_const, :VERSION)
12
+ Haml::VERSION = #{version.inspect}
13
+ require 'opal/rails/haml_filter'
14
+ RUBY
15
+
16
+ Open3.capture3('bundle', '_4.0.3_', 'exec', RbConfig.ruby, '-I', File.join(project_root, 'lib'), '-e', script,
17
+ chdir: project_root)
18
+ end
19
+
20
+ it 'rejects Haml 5.x' do
21
+ _stdout, stderr, status = run_loader_with_version('5.2.2')
22
+
23
+ expect(status).not_to be_success
24
+ expect(stderr).to include('Haml 6 or newer')
25
+ end
26
+
27
+ it 'loads on Haml 6+' do
28
+ _stdout, _, status = run_loader_with_version('6.3.0')
29
+
30
+ expect(status).to be_success
31
+ end
32
+
33
+ it 'returns the module script mime type when ESM is enabled' do
34
+ original_esm = Opal::Config.esm
35
+ Opal::Config.esm = true
36
+
37
+ expect(Haml::Filters::Opal.allocate.mime_type).to eq('module')
38
+ ensure
39
+ Opal::Config.esm = original_esm
40
+ end
41
+ end
@@ -0,0 +1,94 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Opal::Rails::LegacyUpgradeWarning do
4
+ around do |example|
5
+ Dir.mktmpdir do |dir|
6
+ @root = Pathname(dir)
7
+ FileUtils.mkdir_p(@root.join('config/initializers'))
8
+ FileUtils.mkdir_p(@root.join('app/assets/config'))
9
+ example.run
10
+ end
11
+ end
12
+
13
+ it 'warns for the reproduced 2.x generator layout' do
14
+ @root.join('config/initializers/opal.rb').write(<<~RUBY)
15
+ Rails.application.configure do
16
+ config.opal.method_missing_enabled = true
17
+ config.opal.const_missing_enabled = true
18
+ config.opal.arity_check_enabled = false
19
+ config.opal.freezing_stubs_enabled = true
20
+ config.opal.dynamic_require_severity = :warning
21
+ config.opal.assigns_in_templates = false
22
+ end
23
+ RUBY
24
+ @root.join('app/assets/config/manifest.js').write("//= link_directory ../javascript .js\n")
25
+ FileUtils.mkdir_p(@root.join('app/assets/javascript'))
26
+ @root.join('app/assets/javascript/application.js.rb').write("require 'opal'\n")
27
+
28
+ warning = described_class.warning_for(@root)
29
+
30
+ expect(warning).to include('likely 2.x application layout')
31
+ expect(warning).to include('Pin `opal-rails` to the 2.0 series')
32
+ expect(warning).to include('config.opal.suppress_legacy_upgrade_warning = true')
33
+ expect(warning).to include('config/initializers/opal.rb still uses legacy 2.x runtime settings')
34
+ expect(warning).to include('legacy Opal asset entrypoint present at app/assets/javascript/application.js.rb')
35
+ expect(warning).to include('app/assets/config/manifest.js still links the legacy javascript asset tree')
36
+ expect(warning).to include('expected 3.x source root app/opal is missing')
37
+ end
38
+
39
+ it 'does not warn for a build-based 3.x layout' do
40
+ @root.join('config/initializers/opal.rb').write(<<~RUBY)
41
+ Rails.application.configure do
42
+ config.opal.source_path = Rails.root.join('app/opal')
43
+ config.opal.entrypoints_path = config.opal.source_path
44
+ config.opal.build_path = Rails.root.join('app/assets/builds')
45
+ config.opal.entrypoints = { 'application' => 'application.rb' }
46
+ end
47
+ RUBY
48
+ FileUtils.mkdir_p(@root.join('app/opal'))
49
+
50
+ expect(described_class.warning_for(@root)).to be_nil
51
+ end
52
+
53
+ it 'does not warn from a single weak signal' do
54
+ @root.join('config/initializers/opal.rb').write(<<~RUBY)
55
+ Rails.application.configure do
56
+ config.opal.method_missing_enabled = true
57
+ end
58
+ RUBY
59
+
60
+ expect(described_class.warning_for(@root)).to be_nil
61
+ end
62
+
63
+ it 'writes the warning to the provided output' do
64
+ @root.join('config/initializers/opal.rb').write(<<~RUBY)
65
+ Rails.application.configure do
66
+ config.opal.method_missing_enabled = true
67
+ config.opal.const_missing_enabled = true
68
+ end
69
+ RUBY
70
+ @root.join('app/assets/config/manifest.js').write("//= link_directory ../javascript .js\n")
71
+
72
+ output = StringIO.new
73
+
74
+ expect(described_class.warn_if_needed(Struct.new(:root).new(@root), output: output)).to be(true)
75
+ expect(output.string).to include('likely 2.x application layout')
76
+ end
77
+
78
+ it 'does not write the warning when suppression is enabled' do
79
+ @root.join('config/initializers/opal.rb').write(<<~RUBY)
80
+ Rails.application.configure do
81
+ config.opal.method_missing_enabled = true
82
+ config.opal.const_missing_enabled = true
83
+ end
84
+ RUBY
85
+ @root.join('app/assets/config/manifest.js').write("//= link_directory ../javascript .js\n")
86
+
87
+ output = StringIO.new
88
+ app = Struct.new(:root, :config).new(@root,
89
+ Struct.new(:opal).new(Struct.new(:suppress_legacy_upgrade_warning).new(true)))
90
+
91
+ expect(described_class.warn_if_needed(app, output: output)).to be(false)
92
+ expect(output.string).to be_empty
93
+ end
94
+ end
@@ -0,0 +1,44 @@
1
+ require 'spec_helper'
2
+ require 'tmpdir'
3
+ require 'fileutils'
4
+
5
+ RSpec.describe Opal::Rails::OutputsManifest do
6
+ around do |example|
7
+ Dir.mktmpdir do |dir|
8
+ @build_path = Pathname(dir)
9
+ FileUtils.mkdir_p(@build_path)
10
+ example.run
11
+ end
12
+ end
13
+
14
+ it 'prunes only previously tracked outputs' do
15
+ @build_path.join('application.js').write('old')
16
+ @build_path.join('application.js.map').write('old map')
17
+ @build_path.join('unrelated.js').write('keep me')
18
+
19
+ manifest = described_class.new(build_path: @build_path)
20
+ manifest.write!(%w[application.js application.js.map])
21
+
22
+ manifest.prune_stale!(['application.js'])
23
+ manifest.write!(['application.js'])
24
+
25
+ expect(@build_path.join('application.js')).to exist
26
+ expect(@build_path.join('application.js.map')).not_to exist
27
+ expect(@build_path.join('unrelated.js')).to exist
28
+ end
29
+
30
+ it 'ignores paths that escape the build directory' do
31
+ outside_file = @build_path.join('..', 'outside.txt')
32
+ outside_file.write('should survive')
33
+
34
+ manifest = described_class.new(build_path: @build_path)
35
+ manifest.write!(['../outside.txt', 'application.js'])
36
+
37
+ @build_path.join('application.js').write('app')
38
+
39
+ manifest.prune_stale!(['application.js'])
40
+
41
+ expect(outside_file).to exist
42
+ expect(@build_path.join('application.js')).to exist
43
+ end
44
+ end
@@ -0,0 +1,71 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Opal::Rails::PathSetup do
4
+ FakeAssetsConfig = Struct.new(:excluded_paths)
5
+ FakeConfig = Struct.new(:opal, :eager_load_paths, :assets)
6
+ FakeApp = Struct.new(:root, :config)
7
+
8
+ class FakeAutoloader
9
+ attr_reader :ignored_paths
10
+
11
+ def initialize
12
+ @ignored_paths = []
13
+ end
14
+
15
+ def ignore(path)
16
+ @ignored_paths << path
17
+ end
18
+ end
19
+
20
+ around do |example|
21
+ Dir.mktmpdir do |dir|
22
+ @root = Pathname(dir)
23
+ FileUtils.mkdir_p(@root.join('app/assets/opal'))
24
+ FileUtils.mkdir_p(@root.join('app/views'))
25
+ FileUtils.mkdir_p(@root.join('app/models'))
26
+ example.run
27
+ end
28
+ end
29
+
30
+ it 'defaults to app/opal and ignores it in autoloaders' do
31
+ config = FakeConfig.new(ActiveSupport::OrderedOptions.new, [
32
+ @root.join('app/assets').to_s,
33
+ @root.join('app/views').to_s,
34
+ @root.join('app/opal').to_s,
35
+ @root.join('app/models').to_s
36
+ ], FakeAssetsConfig.new([]))
37
+
38
+ app = FakeApp.new(@root, config)
39
+ autoloaders = [FakeAutoloader.new, FakeAutoloader.new]
40
+
41
+ described_class.apply!(app, autoloaders: autoloaders)
42
+
43
+ expect(config.opal.source_path).to eq(@root.join('app/opal'))
44
+ expect(config.opal.entrypoints_path).to eq(@root.join('app/opal'))
45
+ expect(config.opal.build_path).to eq(@root.join('app/assets/builds'))
46
+ expect(config.eager_load_paths).to eq([
47
+ @root.join('app/models').to_s
48
+ ])
49
+ expect(autoloaders.map(&:ignored_paths)).to all(include(@root.join('app/opal')))
50
+ expect(config.assets.excluded_paths).to be_empty
51
+ end
52
+
53
+ it 'leaves app/assets/opal visible to the asset pipeline without autoloader ignores' do
54
+ config = FakeConfig.new(ActiveSupport::OrderedOptions.new, [
55
+ @root.join('app/assets').to_s,
56
+ @root.join('app/views').to_s,
57
+ @root.join('app/models').to_s
58
+ ], FakeAssetsConfig.new([]))
59
+ config.opal.source_path = @root.join('app/assets/opal')
60
+
61
+ app = FakeApp.new(@root, config)
62
+ autoloaders = [FakeAutoloader.new]
63
+
64
+ 2.times { described_class.apply!(app, autoloaders: autoloaders) }
65
+
66
+ expect(config.opal.entrypoints_path).to eq(@root.join('app/assets/opal'))
67
+ expect(config.opal.build_path).to eq(@root.join('app/assets/builds'))
68
+ expect(config.assets.excluded_paths).to be_empty
69
+ expect(autoloaders.first.ignored_paths).to be_empty
70
+ end
71
+ end
@@ -0,0 +1,61 @@
1
+ require 'spec_helper'
2
+ require 'rake'
3
+
4
+ RSpec.describe Opal::Rails::TaskHooks do
5
+ let(:rake_application) { Rake::Application.new }
6
+
7
+ before do
8
+ Rake.application = rake_application
9
+ rake_application.define_task(Rake::Task, 'opal:build')
10
+ rake_application.define_task(Rake::Task, 'opal:clobber')
11
+ end
12
+
13
+ after do
14
+ Rake.application = nil
15
+ end
16
+
17
+ it 'attaches opal:build to known hook tasks when they exist' do
18
+ rake_application.define_task(Rake::Task, 'assets:precompile')
19
+ rake_application.define_task(Rake::Task, 'test:prepare')
20
+
21
+ described_class.apply!(task_manager: Rake::Task)
22
+
23
+ expect(Rake::Task['assets:precompile'].prerequisites).to include('opal:build')
24
+ expect(Rake::Task['test:prepare'].prerequisites).to include('opal:build')
25
+ end
26
+
27
+ it 'does not duplicate prerequisites when applied more than once' do
28
+ rake_application.define_task(Rake::Task, 'assets:precompile')
29
+
30
+ 2.times { described_class.apply!(task_manager: Rake::Task) }
31
+
32
+ expect(Rake::Task['assets:precompile'].prerequisites.count('opal:build')).to eq(1)
33
+ end
34
+
35
+ it 'allows later task definitions to be hooked by reapplying' do
36
+ described_class.apply!(task_manager: Rake::Task)
37
+ rake_application.define_task(Rake::Task, 'spec:prepare')
38
+
39
+ described_class.apply!(task_manager: Rake::Task)
40
+
41
+ expect(Rake::Task['spec:prepare'].prerequisites).to include('opal:build')
42
+ end
43
+
44
+ it 'attaches opal:clobber to assets:clobber when it exists' do
45
+ rake_application.define_task(Rake::Task, 'assets:clobber')
46
+
47
+ described_class.apply!(task_manager: Rake::Task)
48
+
49
+ expect(Rake::Task['assets:clobber'].prerequisites).to include('opal:clobber')
50
+ end
51
+
52
+ it 'attaches test:prepare to the test task so rake test triggers opal:build' do
53
+ rake_application.define_task(Rake::Task, 'test')
54
+ rake_application.define_task(Rake::Task, 'test:prepare')
55
+
56
+ described_class.apply!(task_manager: Rake::Task)
57
+
58
+ expect(Rake::Task['test'].prerequisites).to include('test:prepare')
59
+ expect(Rake::Task['test:prepare'].prerequisites).to include('opal:build')
60
+ end
61
+ end