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.
- checksums.yaml +4 -4
- data/.github/workflows/build.yml +39 -4
- data/.gitignore +5 -0
- data/Appraisals +20 -21
- data/CHANGELOG.md +36 -0
- data/Gemfile +2 -2
- data/PORTING.md +140 -0
- data/README.md +151 -88
- data/Rakefile +16 -2
- data/app/helpers/opal_helper.rb +9 -28
- data/bin/sandbox +3 -2
- data/bin/sandbox-setup +2 -0
- data/gemfiles/{rails_7_0_opal_1_0.gemfile → rails_7_0_opal_1_8.gemfile} +2 -2
- data/gemfiles/{rails_7_0_opal_1_3.gemfile → rails_7_0_opal_master.gemfile} +2 -2
- data/gemfiles/{rails_6_0_opal_1_0.gemfile → rails_8_0_opal_1_8.gemfile} +3 -3
- data/gemfiles/rails_8_0_opal_master.gemfile +10 -0
- data/gemfiles/{rails_6_1_opal_1_0.gemfile → rails_8_1_opal_1_8.gemfile} +3 -3
- data/gemfiles/rails_8_1_opal_master.gemfile +10 -0
- data/lib/generators/opal/assets/assets_generator.rb +11 -1
- data/lib/generators/opal/assets/templates/{javascript.js.rb → asset.rb.tt} +3 -6
- data/lib/generators/opal/install/install_generator.rb +160 -9
- data/lib/generators/opal/install/templates/{application.js.rb → application.rb} +4 -4
- data/lib/generators/opal/install/templates/dev.tt +8 -0
- data/lib/generators/opal/install/templates/{initializer.rb → initializer.rb.tt} +8 -0
- data/lib/opal/rails/builder_runner.rb +126 -0
- data/lib/opal/rails/engine.rb +11 -9
- data/lib/opal/rails/entrypoints_resolver.rb +81 -0
- data/lib/opal/rails/errors.rb +11 -0
- data/lib/opal/rails/file_watcher.rb +58 -0
- data/lib/opal/rails/haml6_filter.rb +6 -6
- data/lib/opal/rails/haml_filter.rb +3 -6
- data/lib/opal/rails/legacy_upgrade_warning.rb +95 -0
- data/lib/opal/rails/outputs_manifest.rb +82 -0
- data/lib/opal/rails/path_setup.rb +28 -0
- data/lib/opal/rails/task_hooks.rb +49 -0
- data/lib/opal/rails/version.rb +1 -1
- data/lib/opal/rails/watch_runner.rb +173 -0
- data/lib/opal/rails.rb +7 -0
- data/lib/tasks/opal.rake +48 -0
- data/opal-rails.gemspec +23 -14
- data/spec/end_to_end/full_lifecycle_spec.rb +377 -0
- data/spec/helpers/opal_helper_spec.rb +27 -34
- data/spec/integration/source_map_spec.rb +6 -8
- data/spec/opal/assets_generator_spec.rb +31 -0
- data/spec/opal/install_generator_spec.rb +140 -0
- data/spec/opal/rails/build_task_spec.rb +116 -0
- data/spec/opal/rails/builder_runner_spec.rb +151 -0
- data/spec/opal/rails/clobber_task_spec.rb +55 -0
- data/spec/opal/rails/entrypoints_resolver_spec.rb +50 -0
- data/spec/opal/rails/haml_filter_spec.rb +41 -0
- data/spec/opal/rails/legacy_upgrade_warning_spec.rb +94 -0
- data/spec/opal/rails/outputs_manifest_spec.rb +44 -0
- data/spec/opal/rails/path_setup_spec.rb +71 -0
- data/spec/opal/rails/task_hooks_spec.rb +61 -0
- data/spec/opal/rails/watch_runner_spec.rb +283 -0
- data/spec/opal/rails/watch_task_spec.rb +23 -0
- data/spec/spec_helper.rb +16 -7
- data/spec/support/browser_support_spec.rb +36 -0
- data/spec/support/capybara.rb +61 -0
- data/spec/support/reset_assets_cache.rb +9 -1
- data/spec/support/test_app.rb +23 -2
- data/test_apps/app/application_controller.rb +23 -32
- data/test_apps/app/assets/builds/.keep +0 -0
- data/test_apps/app/assets/config/manifest.js +3 -1
- data/test_apps/app/assets/images/.keep +0 -0
- data/test_apps/app/opal/application.rb +5 -0
- data/test_apps/app/{assets/javascripts/source_map_example.js.rb → opal/source_map_example.rb} +1 -2
- data/test_apps/app/opal/with_assignments.js.rb +8 -0
- data/test_apps/rails.rb +19 -5
- metadata +196 -50
- data/gemfiles/rails_6_0_opal_1_1.gemfile +0 -9
- data/gemfiles/rails_6_0_opal_1_3.gemfile +0 -10
- data/gemfiles/rails_6_0_opal_1_7.gemfile +0 -10
- data/gemfiles/rails_6_1_opal_1_1.gemfile +0 -9
- data/gemfiles/rails_6_1_opal_1_3.gemfile +0 -10
- data/gemfiles/rails_6_1_opal_1_7.gemfile +0 -10
- data/gemfiles/rails_7_0_opal_1_7.gemfile +0 -10
- data/lib/opal/rails/haml5_filter.rb +0 -28
- data/test_apps/app/assets/javascripts/application.js.rb +0 -7
- data/test_apps/app/assets/javascripts/bar.rb +0 -3
- 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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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.
|
|
6
|
+
let(:js_asset_path) { '/assets/source_map_example.js' }
|
|
7
7
|
|
|
8
8
|
before do
|
|
9
|
-
|
|
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 =
|
|
25
|
+
map_path = comment_map_path&.strip
|
|
26
|
+
expect(map_path).to be_present
|
|
29
27
|
|
|
30
|
-
get URI.join(
|
|
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
|