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,377 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+ require 'net/http'
5
+ require 'fileutils'
6
+ require 'tmpdir'
7
+ require 'pathname'
8
+ require 'timeout'
9
+
10
+ # End-to-end lifecycle test for opal-rails.
11
+ #
12
+ # Creates a fresh Rails app (no flags skipped), installs opal-rails,
13
+ # writes Opal code, and exercises the full development -> test -> production
14
+ # cycle. Opal assets are never built explicitly; every build is triggered
15
+ # implicitly through opal:watch (via bin/dev), test:prepare (via rails test),
16
+ # or assets:precompile (via rails assets:precompile).
17
+ #
18
+ # Run with: bundle exec rspec spec/end_to_end/ --tag e2e
19
+ #
20
+ # Excluded from the default suite because it is slow (~60-90s).
21
+ RSpec.describe 'Full opal-rails lifecycle', :e2e do
22
+ GEM_ROOT = Pathname(__dir__).join('../..').expand_path.freeze
23
+
24
+ # ---------------------------------------------------------------------------
25
+ # Helpers
26
+ # ---------------------------------------------------------------------------
27
+
28
+ def run!(cmd, env: {}, chdir: @app_root.to_s, label: cmd.to_s, timeout: 120)
29
+ full_env = {
30
+ 'BUNDLE_GEMFILE' => @app_root.join('Gemfile').to_s,
31
+ 'RAILS_ENV' => nil,
32
+ 'RACK_ENV' => nil
33
+ }.merge(env)
34
+
35
+ stdout, stderr, status = nil
36
+ Timeout.timeout(timeout) do
37
+ stdout, stderr, status = Open3.capture3(full_env, *Array(cmd), chdir: chdir.to_s)
38
+ end
39
+
40
+ unless status.success?
41
+ raise "Command failed: #{label}\nExit: #{status.exitstatus}\n" \
42
+ "STDOUT:\n#{stdout}\nSTDERR:\n#{stderr}"
43
+ end
44
+
45
+ stdout
46
+ end
47
+
48
+ def unbundled_run!(cmd, **kwargs)
49
+ Bundler.with_unbundled_env do
50
+ run!(cmd, **kwargs)
51
+ end
52
+ end
53
+
54
+ def write_file(relative_path, content)
55
+ path = @app_root.join(relative_path)
56
+ path.dirname.mkpath
57
+ path.write(content)
58
+ end
59
+
60
+ def read_file(relative_path)
61
+ @app_root.join(relative_path).read
62
+ end
63
+
64
+ # Start the app with bin/dev (Foreman: web + opal:watch).
65
+ # Spawns as a process group so we can kill the whole tree.
66
+ def start_dev
67
+ @port = find_available_port
68
+ dev_env = {
69
+ 'BUNDLE_GEMFILE' => @app_root.join('Gemfile').to_s,
70
+ 'PORT' => @port.to_s,
71
+ 'RAILS_ENV' => 'development'
72
+ }
73
+
74
+ @app_root.join('log').mkpath
75
+
76
+ Bundler.with_unbundled_env do
77
+ @dev_pid = spawn(
78
+ dev_env,
79
+ 'bin/dev',
80
+ chdir: @app_root.to_s,
81
+ pgroup: true,
82
+ out: @app_root.join('log/dev_stdout.log').to_s,
83
+ err: @app_root.join('log/dev_stderr.log').to_s
84
+ )
85
+ end
86
+
87
+ wait_for_server!
88
+ wait_for_opal_build!
89
+ end
90
+
91
+ # Start the app with bin/rails server (production mode).
92
+ def start_server(env: 'production', port: nil)
93
+ @port = port || find_available_port
94
+ server_env = {
95
+ 'BUNDLE_GEMFILE' => @app_root.join('Gemfile').to_s,
96
+ 'RAILS_ENV' => env,
97
+ 'PORT' => @port.to_s,
98
+ 'SECRET_KEY_BASE' => 'test_secret_key_base_for_e2e_0123456789abcdef' * 2,
99
+ 'RAILS_SERVE_STATIC_FILES' => '1',
100
+ 'RAILS_LOG_TO_STDOUT' => '0'
101
+ }
102
+
103
+ @app_root.join('log').mkpath
104
+
105
+ Bundler.with_unbundled_env do
106
+ @dev_pid = spawn(
107
+ server_env,
108
+ 'ruby', 'bin/rails', 'server', '-b', '127.0.0.1', '-p', @port.to_s,
109
+ chdir: @app_root.to_s,
110
+ pgroup: true,
111
+ out: @app_root.join('log/server_stdout.log').to_s,
112
+ err: @app_root.join('log/server_stderr.log').to_s
113
+ )
114
+ end
115
+
116
+ wait_for_server!
117
+ end
118
+
119
+ def stop_dev
120
+ return unless @dev_pid
121
+
122
+ # Kill the entire process group (foreman + children)
123
+ Process.kill('-TERM', @dev_pid)
124
+ Process.waitpid(@dev_pid)
125
+ @dev_pid = nil
126
+ rescue Errno::ESRCH, Errno::ECHILD
127
+ @dev_pid = nil
128
+ end
129
+
130
+ alias_method :stop_server, :stop_dev
131
+
132
+ def wait_for_server!(timeout: 60)
133
+ deadline = Time.now + timeout
134
+ loop do
135
+ Net::HTTP.get(URI("http://127.0.0.1:#{@port}/"))
136
+ return
137
+ rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Net::ReadTimeout, EOFError
138
+ raise "Server did not start within #{timeout}s" if Time.now > deadline
139
+
140
+ sleep 0.5
141
+ end
142
+ end
143
+
144
+ # Wait for opal:watch to produce the initial build.
145
+ def wait_for_opal_build!(timeout: 60)
146
+ deadline = Time.now + timeout
147
+ js_path = @app_root.join('app/assets/builds/application.js')
148
+ loop do
149
+ return if js_path.exist? && js_path.size > 0
150
+ raise "opal:watch did not produce application.js within #{timeout}s" if Time.now > deadline
151
+
152
+ sleep 0.5
153
+ end
154
+ end
155
+
156
+ # Wait for the built JS to contain the expected content (watch rebuild).
157
+ def wait_for_js_content!(marker, timeout: 30)
158
+ deadline = Time.now + timeout
159
+ js_path = @app_root.join('app/assets/builds/application.js')
160
+ loop do
161
+ if js_path.exist? && js_path.read.include?(marker)
162
+ # Give server a moment to pick up the new file
163
+ sleep 0.5
164
+ return
165
+ end
166
+ raise "Built JS did not contain '#{marker}' within #{timeout}s" if Time.now > deadline
167
+
168
+ sleep 0.5
169
+ end
170
+ end
171
+
172
+ def http_get(path = '/')
173
+ uri = URI("http://127.0.0.1:#{@port}#{path}")
174
+ Net::HTTP.start(uri.host, uri.port, read_timeout: 10) do |http|
175
+ response = http.get(uri.path)
176
+ response = http.get(URI(response['location']).path) if response.is_a?(Net::HTTPRedirection)
177
+ response
178
+ end
179
+ end
180
+
181
+ def find_available_port
182
+ server = TCPServer.new('127.0.0.1', 0)
183
+ port = server.addr[1]
184
+ server.close
185
+ port
186
+ end
187
+
188
+ # Fetch the HTML page, extract JS asset URLs, fetch each, and return
189
+ # the concatenation of all JS bodies.
190
+ def fetch_js_content(path = '/')
191
+ html = http_get(path).body
192
+ js_urls = html.scan(/src="([^"]*\.js[^"]*)"/).flatten
193
+ js_urls.map { |url| http_get(url).body }.join("\n")
194
+ end
195
+
196
+ # ---------------------------------------------------------------------------
197
+ # Setup / Teardown
198
+ # ---------------------------------------------------------------------------
199
+
200
+ around do |example|
201
+ Dir.mktmpdir('opal_e2e_') do |dir|
202
+ @app_root = Pathname(dir).join('testapp')
203
+ example.run
204
+ end
205
+ ensure
206
+ stop_dev
207
+ end
208
+
209
+ # ---------------------------------------------------------------------------
210
+ # The test
211
+ # ---------------------------------------------------------------------------
212
+
213
+ it 'exercises the full development, test, and production lifecycle' do
214
+ # -----------------------------------------------------------------------
215
+ # Phase 1: Create a fresh Rails application (no --skip flags)
216
+ # -----------------------------------------------------------------------
217
+ Bundler.with_unbundled_env do
218
+ run!(
219
+ ['rails', 'new', @app_root.to_s, '--skip-bundle', '--skip-git'],
220
+ chdir: Dir.tmpdir,
221
+ label: 'rails new'
222
+ )
223
+ end
224
+ expect(@app_root.join('config/application.rb')).to exist
225
+
226
+ # Add opal-rails to the Gemfile
227
+ gemfile = read_file('Gemfile')
228
+ write_file('Gemfile', gemfile + "\ngem 'opal-rails', path: '#{GEM_ROOT}'\n")
229
+
230
+ unbundled_run!('bundle install', label: 'bundle install', timeout: 180)
231
+
232
+ # -----------------------------------------------------------------------
233
+ # Phase 2: Install opal-rails and set up a basic app
234
+ # -----------------------------------------------------------------------
235
+ unbundled_run!('ruby bin/rails generate opal:install', label: 'opal:install')
236
+ unbundled_run!('ruby bin/rails generate controller home index -f', label: 'generate controller')
237
+
238
+ # Set the root route
239
+ write_file('config/routes.rb', <<~RUBY)
240
+ Rails.application.routes.draw do
241
+ root to: "home#index"
242
+ end
243
+ RUBY
244
+
245
+ # Write initial Opal code
246
+ write_file('app/opal/application.rb', <<~RUBY)
247
+ # backtick_javascript: true
248
+ require 'opal'
249
+ `window.opalMarker = "version_1"`
250
+ RUBY
251
+
252
+ # -----------------------------------------------------------------------
253
+ # Phase 3: Start in development mode via bin/dev and verify
254
+ # (opal:watch performs the initial build implicitly)
255
+ # -----------------------------------------------------------------------
256
+ start_dev
257
+
258
+ html = http_get('/').body
259
+ expect(html).to include('<script')
260
+
261
+ js = fetch_js_content('/')
262
+ expect(js).to include('version_1')
263
+
264
+ # -----------------------------------------------------------------------
265
+ # Phase 4: Modify Opal code, let opal:watch rebuild, verify live
266
+ # -----------------------------------------------------------------------
267
+ write_file('app/opal/application.rb', <<~RUBY)
268
+ # backtick_javascript: true
269
+ require 'opal'
270
+ `window.opalMarker = "version_2"`
271
+ RUBY
272
+
273
+ wait_for_js_content!('version_2')
274
+
275
+ js = fetch_js_content('/')
276
+ expect(js).to include('version_2')
277
+ expect(js).not_to include('version_1')
278
+
279
+ # -----------------------------------------------------------------------
280
+ # Phase 5: Stop bin/dev, modify code, restart bin/dev, verify
281
+ # (opal:watch initial build picks up the offline changes)
282
+ # -----------------------------------------------------------------------
283
+ stop_dev
284
+
285
+ write_file('app/opal/application.rb', <<~RUBY)
286
+ # backtick_javascript: true
287
+ require 'opal'
288
+ `window.opalMarker = "version_3"`
289
+ RUBY
290
+
291
+ start_dev
292
+ wait_for_js_content!('version_3')
293
+
294
+ js = fetch_js_content('/')
295
+ expect(js).to include('version_3')
296
+ expect(js).not_to include('version_2')
297
+
298
+ # -----------------------------------------------------------------------
299
+ # Phase 6: Stop bin/dev, modify code, run tests
300
+ # (test:prepare triggers opal:build implicitly)
301
+ # -----------------------------------------------------------------------
302
+ stop_dev
303
+
304
+ write_file('app/opal/application.rb', <<~RUBY)
305
+ # backtick_javascript: true
306
+ require 'opal'
307
+ `window.opalMarker = "version_4"`
308
+ RUBY
309
+
310
+ # Create a simple integration test that checks the built JS content
311
+ write_file('test/integration/opal_test.rb', <<~RUBY)
312
+ require "test_helper"
313
+
314
+ class OpalIntegrationTest < ActionDispatch::IntegrationTest
315
+ test "opal assets contain the expected marker" do
316
+ get "/"
317
+ assert_response :success
318
+ body = response.body
319
+
320
+ # Extract JS asset src from the page
321
+ js_src = body.scan(/src="([^"]*application[^"]*\\.js[^"]*)"/).flatten.first
322
+ assert js_src, "Expected to find a JS script tag for application"
323
+
324
+ get js_src
325
+ assert_response :success
326
+ assert_includes response.body, "version_4", "Expected the JS to contain version_4"
327
+ end
328
+ end
329
+ RUBY
330
+
331
+ # Remove the scaffold-generated controller test — its named route
332
+ # (home_index_url) no longer exists because we replaced routes.rb.
333
+ FileUtils.rm_f(@app_root.join('test/controllers/home_controller_test.rb').to_s)
334
+
335
+ test_output = unbundled_run!(
336
+ ['ruby', 'bin/rails', 'test'],
337
+ env: { 'RAILS_ENV' => 'test' },
338
+ label: 'rails test'
339
+ )
340
+
341
+ expect(test_output).to match(/1 (test|run)/)
342
+ expect(test_output).to match(/0 failures/)
343
+
344
+ # -----------------------------------------------------------------------
345
+ # Phase 7: Modify code, assets:precompile, run production
346
+ # (assets:precompile triggers opal:build implicitly)
347
+ # -----------------------------------------------------------------------
348
+ write_file('app/opal/application.rb', <<~RUBY)
349
+ # backtick_javascript: true
350
+ require 'opal'
351
+ `window.opalMarker = "version_5_production"`
352
+ RUBY
353
+
354
+ unbundled_run!(
355
+ 'ruby bin/rails assets:precompile',
356
+ env: { 'RAILS_ENV' => 'production' },
357
+ label: 'assets:precompile'
358
+ )
359
+
360
+ # Verify precompiled assets exist and contain our marker
361
+ public_assets = Dir.glob(@app_root.join('public/assets/**/*.js').to_s)
362
+ expect(public_assets).not_to be_empty, 'Expected precompiled JS assets in public/assets/'
363
+
364
+ production_js = public_assets.map { |f| File.read(f) }.join("\n")
365
+ expect(production_js).to include('version_5_production')
366
+
367
+ start_server(env: 'production')
368
+
369
+ html = http_get('/').body
370
+ expect(html).to include('<script')
371
+
372
+ js = fetch_js_content('/')
373
+ expect(js).to include('version_5_production')
374
+
375
+ stop_server
376
+ end
377
+ end
@@ -6,45 +6,38 @@ describe OpalHelper, :js, type: :view do
6
6
  let(:helper) { view }
7
7
 
8
8
  describe '#opal_tag' do
9
- it 'compiles to js' do
10
- allow(helper).to receive(:javascript_tag) { |code| code }
11
- ruby_code = 'puts 5'
12
-
13
- expect(Opal::Compiler).to receive(:new)
9
+ let(:ruby_code) { 'puts 5' }
10
+ let(:compiled_ruby_code) { 'self.$puts(5)' }
11
+ let(:html_options) { { async: true } }
12
+ before do
13
+ allow(helper).to receive(:javascript_tag).and_call_original
14
+ allow(Opal::Compiler).to receive(:new)
14
15
  .with(ruby_code, hash_including(requirable: false))
15
16
  .and_call_original
16
-
17
- expect(helper.opal_tag(ruby_code)).to include('self.$puts(5)')
18
17
  end
19
- end
20
-
21
- specify '#javascript_include_tag' do
22
- # sprockets-rails v3 sets Rails.application.assets to nil in production mode
23
- allow(Rails.application).to receive(:assets).and_return(nil)
24
-
25
- loading_code = [
26
- %<if(window.Opal && Opal.modules["application"]){Opal.loaded(typeof(OpalLoaded) === "undefined" ? [] : OpalLoaded);>,
27
- %<Opal.require("application");}>,
28
- ].join("\n")
29
18
 
30
- escaped_loading_code = ERB::Util.h loading_code
31
- loading_code_in_script_tag = [
32
- %(<script>), %(//<![CDATA[), loading_code, %(//]]>), %(</script>),
33
- ].join("\n")
19
+ context 'when the ruby code is passed inline' do
20
+ it 'compiles the ruby code to js' do
21
+ expect(helper.opal_tag(ruby_code)).to include(compiled_ruby_code)
22
+ end
34
23
 
35
- expect(helper.javascript_include_tag('application', debug: true)).to include(loading_code_in_script_tag)
36
- expect(helper.javascript_include_tag('application', debug: true)).not_to include(escaped_loading_code)
37
-
38
- expect(helper.javascript_include_tag('application', debug: false)).to include(escaped_loading_code)
39
- expect(helper.javascript_include_tag('application', debug: false)).not_to include(loading_code_in_script_tag)
40
-
41
- expect(helper.javascript_include_tag('application', skip_opal_loader: true)).not_to include(escaped_loading_code)
42
- expect(helper.javascript_include_tag('application', skip_opal_loader: false)).to include(loading_code_in_script_tag)
43
-
44
- expect(helper.javascript_include_tag('application', force_opal_loader_tag: true, debug: true)).to include(loading_code_in_script_tag)
45
- expect(helper.javascript_include_tag('application', force_opal_loader_tag: true, debug: false)).to include(loading_code_in_script_tag)
24
+ it 'passes the html_options to the javascript_tag' do
25
+ helper.opal_tag(ruby_code, html_options)
26
+ expect(helper).to have_received(:javascript_tag).with(html_options)
27
+ end
28
+ end
46
29
 
47
- expect(helper.javascript_include_tag('application', force_opal_loader_tag: true, skip_opal_loader: true)).not_to include(escaped_loading_code)
48
- expect(helper.javascript_include_tag('application', force_opal_loader_tag: true, skip_opal_loader: true)).to include(loading_code_in_script_tag)
30
+ context 'when the ruby code is passed as a block' do
31
+ it 'compiles the block to js' do
32
+ expect(helper.opal_tag { ruby_code }).to include(compiled_ruby_code)
33
+ end
34
+
35
+ it 'uses the options as the first argument' do
36
+ aggregate_failures do
37
+ expect(helper.opal_tag(html_options) { ruby_code }).to include(compiled_ruby_code)
38
+ expect(helper).to have_received(:javascript_tag).with(html_options)
39
+ end
40
+ end
41
+ end
49
42
  end
50
43
  end
@@ -3,17 +3,15 @@ require 'spec_helper'
3
3
  require 'opal/source_map'
4
4
 
5
5
  describe Opal::SourceMap do
6
- let(:js_asset_path) { '/assets/source_map_example.debug.js' }
6
+ let(:js_asset_path) { '/assets/source_map_example.js' }
7
7
 
8
8
  before do
9
- expect(Rails.application.config.opal.source_map_enabled).to be_truthy
10
- expect(Rails.application.config.assets.compile).to be_truthy
11
- expect(Rails.application.assets).to be_present
12
- expect(Rails.application.config.assets.debug).to be_truthy
9
+ TestAppAssets.build!
13
10
  end
14
11
 
15
12
  let(:map_body) do
16
13
  get js_asset_path
14
+ expect(response).to be_successful
17
15
 
18
16
  inline_map_prefix = '//# sourceMappingURL=data:application/json;base64,'
19
17
 
@@ -22,12 +20,12 @@ describe Opal::SourceMap do
22
20
  else
23
21
  source_map_regexp = %r{^//[@#] sourceMappingURL=([^\n]+)}
24
22
 
25
- header_map_path = response.headers['X-SourceMap'].presence
26
23
  comment_map_path = response.body.scan(source_map_regexp).flatten.first.to_s.strip.presence
27
24
 
28
- map_path = (header_map_path || comment_map_path)&.strip
25
+ map_path = comment_map_path&.strip
26
+ expect(map_path).to be_present
29
27
 
30
- get URI.join("http://example.com/", js_asset_path, map_path).path
28
+ get URI.join('http://example.com/', js_asset_path, map_path).path
31
29
 
32
30
  expect(response).to be_successful, "url: #{map_path}\nstatus: #{response.status}"
33
31
 
@@ -0,0 +1,31 @@
1
+ require 'rails/generators'
2
+ require 'generators/opal/assets/assets_generator'
3
+
4
+ RSpec.describe Opal::AssetsGenerator do
5
+ around do |example|
6
+ Dir.mktmpdir do |dir|
7
+ @root = Pathname(dir)
8
+ FileUtils.mkdir_p(@root.join('app'))
9
+ example.run
10
+ end
11
+ end
12
+
13
+ it 'generates a build-based asset under app/opal by default' do
14
+ described_class.start(['dashboard'], destination_root: @root.to_s)
15
+
16
+ generated_file = @root.join('app/opal/dashboard.rb')
17
+
18
+ expect(generated_file).to exist
19
+ expect(generated_file.read).to include('Require this file from `app/opal/application.rb`')
20
+ expect(generated_file.read).to include('class DashboardView')
21
+ end
22
+
23
+ it 'reuses app/assets/opal for migration-friendly layouts' do
24
+ FileUtils.mkdir_p(@root.join('app/assets/opal'))
25
+
26
+ described_class.start(['dashboard'], destination_root: @root.to_s)
27
+
28
+ expect(@root.join('app/assets/opal/dashboard.rb')).to exist
29
+ expect(@root.join('app/opal/dashboard.rb')).not_to exist
30
+ end
31
+ end
@@ -0,0 +1,140 @@
1
+ require 'spec_helper'
2
+ require 'rails/generators'
3
+ require 'tmpdir'
4
+ require 'fileutils'
5
+ require 'generators/opal/install/install_generator'
6
+
7
+ RSpec.describe Opal::InstallGenerator do
8
+ around do |example|
9
+ Dir.mktmpdir do |dir|
10
+ root = Pathname(dir)
11
+ FileUtils.mkdir_p(root.join('app/views/layouts'))
12
+ FileUtils.mkdir_p(root.join('app/assets/config'))
13
+ FileUtils.mkdir_p(root.join('config/environments'))
14
+ root.join('app/views/layouts/application.html.erb').write("<html>\n <head>\n </head>\n</html>\n")
15
+ root.join('app/assets/config/manifest.js').write("//= link_tree ../images\n")
16
+ root.join('config/environments/test.rb').write("Rails.application.configure do\nend\n")
17
+ root.join('.gitignore').write('')
18
+
19
+ @root = root
20
+ example.run
21
+ end
22
+ end
23
+
24
+ it 'creates a build-based install in app/opal and wires Sprockets test assets to builds' do
25
+ described_class.start([], destination_root: @root.to_s)
26
+
27
+ expect(@root.join('app/opal/application.rb')).to exist
28
+ expect(@root.join('config/initializers/opal.rb').read).to include("config.opal.source_path = Rails.root.join('app/opal')")
29
+ expect(@root.join('app/assets/builds/.keep')).to exist
30
+ expect(@root.join('Procfile.dev').read).to include('opal: bin/rails opal:watch')
31
+ expect(@root.join('bin/dev')).to exist
32
+ expect(@root.join('bin/dev').read).to include('foreman start -f Procfile.dev "$@"')
33
+ expect(@root.join('.gitignore').read).to include('/app/assets/builds/*')
34
+ expect(@root.join('app/views/layouts/application.html.erb').read).to include('javascript_include_tag "application", "data-turbo-track": "reload"')
35
+ expect(@root.join('app/assets/config/manifest.js').read).to eq("//= link_tree ../images\n//= link_directory ../builds .js\n//= link_directory ../builds .map\n")
36
+ expect(@root.join('config/environments/test.rb').read).to eq("Rails.application.configure do\n config.assets.debug = true\nend\n")
37
+ end
38
+
39
+ it 'keeps app/assets/opal as the source root for migration installs' do
40
+ FileUtils.mkdir_p(@root.join('app/assets/opal'))
41
+
42
+ described_class.start([], destination_root: @root.to_s)
43
+
44
+ expect(@root.join('app/assets/opal/application.rb')).to exist
45
+ expect(@root.join('config/initializers/opal.rb').read).to include("config.opal.source_path = Rails.root.join('app/assets/opal')")
46
+ end
47
+
48
+ it 'uses :all for existing multi-entrypoint migration layouts without adding an application include' do
49
+ FileUtils.mkdir_p(@root.join('app/assets/opal'))
50
+ @root.join('app/assets/opal/sample_selector.rb').write("require 'opal'\n")
51
+ @root.join('app/assets/opal/address_form.rb').write("require 'opal'\n")
52
+
53
+ described_class.start([], destination_root: @root.to_s)
54
+
55
+ expect(@root.join('app/assets/opal/application.rb')).not_to exist
56
+ expect(@root.join('config/initializers/opal.rb').read).to include('config.opal.entrypoints = :all')
57
+ expect(@root.join('app/views/layouts/application.html.erb').read).not_to include('javascript_include_tag "application"')
58
+ end
59
+
60
+ it 'uses an explicit opal asset name when the host app already has application.js' do
61
+ FileUtils.mkdir_p(@root.join('app/javascript'))
62
+ @root.join('app/javascript/application.js').write("console.log('existing app asset')\n")
63
+
64
+ described_class.start([], destination_root: @root.to_s)
65
+
66
+ expect(@root.join('app/opal/application.rb')).to exist
67
+ expect(@root.join('config/initializers/opal.rb').read).to include("config.opal.entrypoints = { 'opal' => 'application.rb' }")
68
+ expect(@root.join('app/views/layouts/application.html.erb').read).to include('javascript_include_tag "opal", "data-turbo-track": "reload"')
69
+ end
70
+
71
+ it 'adds the opal include alongside an existing application include in mixed-stack apps' do
72
+ FileUtils.mkdir_p(@root.join('app/javascript'))
73
+ @root.join('app/javascript/application.js').write("console.log('existing app asset')\n")
74
+ @root.join('app/views/layouts/application.html.erb').write(<<~ERB)
75
+ <html>
76
+ <head>
77
+ <%= javascript_include_tag "application", "data-turbo-track": "reload" %>
78
+ </head>
79
+ </html>
80
+ ERB
81
+
82
+ described_class.start([], destination_root: @root.to_s)
83
+
84
+ layout = @root.join('app/views/layouts/application.html.erb').read
85
+ expect(layout.scan('javascript_include_tag "application"').length).to eq(1)
86
+ expect(layout.scan('javascript_include_tag "opal"').length).to eq(1)
87
+ end
88
+
89
+ it 'does not overwrite an existing application entrypoint' do
90
+ FileUtils.mkdir_p(@root.join('app/opal'))
91
+ @root.join('app/opal/application.rb').write("puts 'keep me'\n")
92
+
93
+ described_class.start([], destination_root: @root.to_s)
94
+
95
+ expect(@root.join('app/opal/application.rb').read).to eq("puts 'keep me'\n")
96
+ end
97
+
98
+ it 'does not add a duplicate application include when the layout already has one' do
99
+ @root.join('app/views/layouts/application.html.erb').write(<<~ERB)
100
+ <html>
101
+ <head>
102
+ <%= javascript_include_tag "application", "data-turbo-track": "reload" %>
103
+ </head>
104
+ </html>
105
+ ERB
106
+
107
+ described_class.start([], destination_root: @root.to_s)
108
+
109
+ expect(@root.join('app/views/layouts/application.html.erb').read.scan('javascript_include_tag "application"').length).to eq(1)
110
+ end
111
+
112
+ it 'does not overwrite an existing bin/dev that already uses Procfile.dev' do
113
+ FileUtils.mkdir_p(@root.join('bin'))
114
+ @root.join('bin/dev').write("#!/usr/bin/env sh\nforeman start -f Procfile.dev\n")
115
+
116
+ described_class.start([], destination_root: @root.to_s)
117
+
118
+ expect(@root.join('bin/dev').read).to eq("#!/usr/bin/env sh\nforeman start -f Procfile.dev\n")
119
+ end
120
+
121
+ it 'replaces bin/dev when it does not use Procfile.dev' do
122
+ FileUtils.mkdir_p(@root.join('bin'))
123
+ @root.join('bin/dev').write("#!/usr/bin/env ruby\nexec \"./bin/rails\", \"server\"\n")
124
+
125
+ described_class.start([], destination_root: @root.to_s)
126
+
127
+ expect(@root.join('bin/dev').read).to include('foreman start -f Procfile.dev "$@"')
128
+ end
129
+
130
+ it 'does not duplicate build links or test asset debug settings' do
131
+ @root.join('app/assets/config/manifest.js').write("//= link_tree ../images\n//= link_directory ../builds .js\n//= link_directory ../builds .map\n")
132
+ @root.join('config/environments/test.rb').write("Rails.application.configure do\n config.assets.debug = true\nend\n")
133
+
134
+ described_class.start([], destination_root: @root.to_s)
135
+
136
+ expect(@root.join('app/assets/config/manifest.js').read.scan('../builds .js').length).to eq(1)
137
+ expect(@root.join('app/assets/config/manifest.js').read.scan('../builds .map').length).to eq(1)
138
+ expect(@root.join('config/environments/test.rb').read.scan('config.assets.debug = true').length).to eq(1)
139
+ end
140
+ end