opal 1.5.1 → 1.6.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (142) hide show
  1. checksums.yaml +4 -4
  2. data/.gitattributes +4 -0
  3. data/.github/workflows/build.yml +17 -3
  4. data/HACKING.md +23 -0
  5. data/README.md +3 -3
  6. data/UNRELEASED.md +47 -0
  7. data/benchmark/run.rb +1 -0
  8. data/docs/compiled_ruby.md +8 -0
  9. data/docs/compiler.md +1 -1
  10. data/docs/compiler_directives.md +1 -1
  11. data/docs/getting_started.md +17 -0
  12. data/docs/headless_chrome.md +1 -1
  13. data/docs/index.md +123 -0
  14. data/docs/jquery.md +5 -5
  15. data/docs/templates.md +37 -37
  16. data/docs/unsupported_features.md +0 -4
  17. data/lib/opal/builder.rb +59 -39
  18. data/lib/opal/builder_processors.rb +24 -0
  19. data/lib/opal/builder_scheduler/prefork.rb +262 -0
  20. data/lib/opal/builder_scheduler/sequential.rb +13 -0
  21. data/lib/opal/builder_scheduler.rb +29 -0
  22. data/lib/opal/cache/file_cache.rb +13 -2
  23. data/lib/opal/cli.rb +36 -19
  24. data/lib/opal/cli_options.rb +4 -0
  25. data/lib/opal/cli_runners/chrome.rb +17 -13
  26. data/lib/opal/cli_runners/chrome_cdp_interface.rb +19 -2
  27. data/lib/opal/cli_runners/compiler.rb +1 -1
  28. data/lib/opal/cli_runners/gjs.rb +3 -1
  29. data/lib/opal/cli_runners/mini_racer.rb +5 -3
  30. data/lib/opal/cli_runners/nodejs.rb +3 -3
  31. data/lib/opal/cli_runners/server.rb +13 -28
  32. data/lib/opal/cli_runners/system_runner.rb +5 -3
  33. data/lib/opal/cli_runners.rb +7 -6
  34. data/lib/opal/compiler.rb +25 -2
  35. data/lib/opal/config.rb +10 -0
  36. data/lib/opal/eof_content.rb +5 -2
  37. data/lib/opal/nodes/args/ensure_kwargs_are_kwargs.rb +2 -6
  38. data/lib/opal/nodes/args/extract_kwarg.rb +3 -4
  39. data/lib/opal/nodes/args/extract_kwargs.rb +3 -1
  40. data/lib/opal/nodes/args/extract_kwoptarg.rb +1 -1
  41. data/lib/opal/nodes/args/extract_kwrestarg.rb +4 -1
  42. data/lib/opal/nodes/args/extract_optarg.rb +1 -1
  43. data/lib/opal/nodes/args/extract_post_arg.rb +1 -1
  44. data/lib/opal/nodes/args/extract_post_optarg.rb +1 -1
  45. data/lib/opal/nodes/args/extract_restarg.rb +2 -2
  46. data/lib/opal/nodes/args/initialize_iterarg.rb +1 -1
  47. data/lib/opal/nodes/args/initialize_shadowarg.rb +1 -1
  48. data/lib/opal/nodes/args/prepare_post_args.rb +4 -2
  49. data/lib/opal/nodes/base.rb +14 -3
  50. data/lib/opal/nodes/call.rb +13 -16
  51. data/lib/opal/nodes/class.rb +3 -1
  52. data/lib/opal/nodes/closure.rb +250 -0
  53. data/lib/opal/nodes/def.rb +7 -11
  54. data/lib/opal/nodes/definitions.rb +4 -2
  55. data/lib/opal/nodes/if.rb +12 -2
  56. data/lib/opal/nodes/iter.rb +11 -17
  57. data/lib/opal/nodes/logic.rb +15 -63
  58. data/lib/opal/nodes/module.rb +3 -1
  59. data/lib/opal/nodes/rescue.rb +23 -15
  60. data/lib/opal/nodes/scope.rb +7 -1
  61. data/lib/opal/nodes/top.rb +27 -4
  62. data/lib/opal/nodes/while.rb +42 -26
  63. data/lib/opal/nodes.rb +1 -0
  64. data/lib/opal/os.rb +59 -0
  65. data/lib/opal/rewriter.rb +2 -0
  66. data/lib/opal/rewriters/returnable_logic.rb +14 -0
  67. data/lib/opal/rewriters/thrower_finder.rb +90 -0
  68. data/lib/opal/simple_server.rb +12 -6
  69. data/lib/opal/source_map/file.rb +4 -3
  70. data/lib/opal/source_map/map.rb +9 -1
  71. data/lib/opal/util.rb +1 -1
  72. data/lib/opal/version.rb +1 -1
  73. data/opal/corelib/array.rb +68 -3
  74. data/opal/corelib/basic_object.rb +1 -0
  75. data/opal/corelib/comparable.rb +1 -1
  76. data/opal/corelib/complex.rb +1 -0
  77. data/opal/corelib/constants.rb +2 -2
  78. data/opal/corelib/enumerable.rb +4 -2
  79. data/opal/corelib/enumerator/chain.rb +4 -0
  80. data/opal/corelib/enumerator/generator.rb +5 -3
  81. data/opal/corelib/enumerator/lazy.rb +3 -1
  82. data/opal/corelib/enumerator/yielder.rb +2 -4
  83. data/opal/corelib/enumerator.rb +3 -1
  84. data/opal/corelib/error/errno.rb +3 -1
  85. data/opal/corelib/error.rb +13 -2
  86. data/opal/corelib/hash.rb +39 -1
  87. data/opal/corelib/io.rb +1 -1
  88. data/opal/corelib/kernel.rb +56 -5
  89. data/opal/corelib/module.rb +60 -4
  90. data/opal/corelib/proc.rb +8 -5
  91. data/opal/corelib/rational.rb +1 -0
  92. data/opal/corelib/regexp.rb +15 -1
  93. data/opal/corelib/runtime.js +307 -238
  94. data/opal/corelib/string/encoding.rb +0 -6
  95. data/opal/corelib/string.rb +28 -7
  96. data/opal/corelib/time.rb +5 -2
  97. data/opal/corelib/unsupported.rb +2 -14
  98. data/opal.gemspec +2 -2
  99. data/spec/filters/bugs/delegate.rb +11 -0
  100. data/spec/filters/bugs/kernel.rb +1 -3
  101. data/spec/filters/bugs/language.rb +3 -23
  102. data/spec/filters/bugs/method.rb +0 -1
  103. data/spec/filters/bugs/module.rb +0 -3
  104. data/spec/filters/bugs/proc.rb +0 -3
  105. data/spec/filters/bugs/set.rb +4 -16
  106. data/spec/filters/bugs/unboundmethod.rb +0 -2
  107. data/spec/filters/unsupported/array.rb +0 -58
  108. data/spec/filters/unsupported/freeze.rb +8 -192
  109. data/spec/filters/unsupported/hash.rb +0 -25
  110. data/spec/filters/unsupported/kernel.rb +0 -1
  111. data/spec/filters/unsupported/privacy.rb +17 -0
  112. data/spec/lib/builder_spec.rb +14 -0
  113. data/spec/lib/cli_runners/server_spec.rb +2 -3
  114. data/spec/lib/cli_spec.rb +15 -1
  115. data/spec/lib/compiler_spec.rb +1 -1
  116. data/spec/opal/core/language/DATA/characters_support_crlf_spec.rb +9 -0
  117. data/spec/opal/core/language/DATA/multiple___END___crlf_spec.rb +10 -0
  118. data/spec/opal/core/language/if_spec.rb +13 -0
  119. data/spec/opal/core/language/safe_navigator_spec.rb +10 -0
  120. data/spec/opal/core/module_spec.rb +8 -0
  121. data/spec/ruby_specs +2 -1
  122. data/stdlib/await.rb +44 -7
  123. data/stdlib/delegate.rb +427 -6
  124. data/stdlib/headless_chrome.rb +6 -2
  125. data/stdlib/nodejs/file.rb +2 -1
  126. data/stdlib/opal-parser.rb +1 -1
  127. data/stdlib/opal-platform.rb +1 -1
  128. data/stdlib/opal-replutils.rb +5 -3
  129. data/stdlib/promise.rb +3 -0
  130. data/stdlib/rbconfig.rb +4 -1
  131. data/stdlib/ruby2_keywords.rb +60 -0
  132. data/stdlib/set.rb +21 -0
  133. data/tasks/performance.rake +41 -35
  134. data/tasks/releasing.rake +1 -0
  135. data/tasks/testing/mspec_special_calls.rb +1 -0
  136. data/tasks/testing.rake +13 -12
  137. data/test/nodejs/test_await.rb +39 -1
  138. data/test/nodejs/test_file.rb +3 -2
  139. metadata +37 -22
  140. data/docs/faq.md +0 -17
  141. data/lib/opal/rewriters/break_finder.rb +0 -36
  142. data/spec/opal/core/kernel/freeze_spec.rb +0 -15
@@ -114,6 +114,12 @@ module Opal
114
114
  end
115
115
  end
116
116
 
117
+ # This handler is for files named ".opalerb", which ought to
118
+ # first get compiled to Ruby code using ERB, then with Opal.
119
+ # Unlike below processors, OpalERBProcessor can be used to
120
+ # compile templates, which will in turn output HTML. Take
121
+ # a look at docs/templates.md to understand this subsystem
122
+ # better.
117
123
  class OpalERBProcessor < RubyProcessor
118
124
  handles :opalerb
119
125
 
@@ -133,6 +139,24 @@ module Opal
133
139
  end
134
140
  end
135
141
 
142
+ # This handler is for files named ".rb.erb", which ought to
143
+ # first get preprocessed via ERB, then via Opal.
144
+ class RubyERBProcessor < RubyProcessor
145
+ handles :"rb.erb"
146
+
147
+ def compiled
148
+ @compiled ||= begin
149
+ @source = ::ERB.new(@source.to_s).result
150
+
151
+ compiler = compiler_for(@source, file: @filename)
152
+ compiler.compile
153
+ compiler
154
+ end
155
+ end
156
+ end
157
+
158
+ # This handler is for files named ".js.erb", which ought to
159
+ # first get preprocessed via ERB, then served verbatim as JS.
136
160
  class ERBProcessor < Processor
137
161
  handles :erb
138
162
 
@@ -0,0 +1,262 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'etc'
4
+
5
+ module Opal
6
+ class BuilderScheduler
7
+ class Prefork < BuilderScheduler
8
+ # We hook into the process_requires method
9
+ def process_requires(rel_path, requires, autoloads, options)
10
+ return if requires.empty?
11
+
12
+ if @in_fork
13
+ io = @in_fork
14
+ io.send(:new_requires, rel_path, requires, autoloads, options)
15
+ else
16
+ prefork_reactor(rel_path, requires, autoloads, options)
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ class ForkSet < Array
23
+ def initialize(count, &block)
24
+ super([])
25
+
26
+ @count, @block = count, block
27
+
28
+ create_fork
29
+ end
30
+
31
+ def get_events(queue_length)
32
+ # Wait for anything to happen:
33
+ # - Either any of our workers return some data
34
+ # - Or any workers become ready to receive data
35
+ # - But only if we have enough work for them
36
+ ios = IO.select(
37
+ map(&:read_io),
38
+ sample(queue_length).map(&:write_io),
39
+ []
40
+ )
41
+ return [[], []] unless ios
42
+
43
+ events = ios[0].map do |io|
44
+ io = from_io(io, :read_io)
45
+ [io, *io.recv]
46
+ end
47
+
48
+ idles = ios[1].map do |io|
49
+ from_io(io, :write_io)
50
+ end
51
+
52
+ # Progressively create forks, because we may not need all
53
+ # the workers at the time. The number 6 was picked due to
54
+ # some trial and error on a Ryzen machine.
55
+ #
56
+ # Do note that prefork may happen more than once.
57
+ create_fork if length < @count && rand(6) == 1
58
+
59
+ [events, idles]
60
+ end
61
+
62
+ def create_fork
63
+ self << Fork.new(self, &@block)
64
+ end
65
+
66
+ def from_io(io, type)
67
+ find { |i| i.__send__(type) == io }
68
+ end
69
+
70
+ def close
71
+ each(&:close)
72
+ end
73
+
74
+ def wait
75
+ each(&:wait)
76
+ end
77
+ end
78
+
79
+ class Fork
80
+ def initialize(forkset)
81
+ @parent_read, @child_write = IO.pipe
82
+ @child_read, @parent_write = IO.pipe
83
+ @forkset = forkset
84
+ @in_fork = false
85
+
86
+ @pid = fork do
87
+ @in_fork = true
88
+
89
+ begin
90
+ @parent_read.close
91
+ @parent_write.close
92
+
93
+ yield(self)
94
+ rescue => error
95
+ send(:exception, error)
96
+ ensure
97
+ send(:close) unless write_io.closed?
98
+ @child_write.close
99
+ end
100
+ end
101
+
102
+ @child_read.close
103
+ @child_write.close
104
+ end
105
+
106
+ def close
107
+ send(:close)
108
+ @parent_write.close
109
+ end
110
+
111
+ def goodbye
112
+ read_io.close unless read_io.closed?
113
+ end
114
+
115
+ def send_message(io, msg)
116
+ msg = Marshal.dump(msg)
117
+ io.write([msg.length].pack('Q') + msg)
118
+ end
119
+
120
+ def recv_message(io)
121
+ length, = *io.read(8).unpack('Q')
122
+ Marshal.load(io.read(length)) # rubocop:disable Security/MarshalLoad
123
+ end
124
+
125
+ def fork?
126
+ @in_fork
127
+ end
128
+
129
+ def read_io
130
+ fork? ? @child_read : @parent_read
131
+ end
132
+
133
+ def write_io
134
+ fork? ? @child_write : @parent_write
135
+ end
136
+
137
+ def eof?
138
+ write_io.closed?
139
+ end
140
+
141
+ def send(*msg)
142
+ send_message(write_io, msg)
143
+ end
144
+
145
+ def recv
146
+ recv_message(read_io)
147
+ end
148
+
149
+ def wait
150
+ Process.waitpid(@pid, Process::WNOHANG)
151
+ end
152
+ end
153
+
154
+ # By default we use 3/4 of CPU threads detected.
155
+ def fork_count
156
+ ENV['OPAL_PREFORK_THREADS']&.to_i || (Etc.nprocessors * 3 / 4.0).ceil
157
+ end
158
+
159
+ def prefork
160
+ @forks = ForkSet.new(fork_count, &method(:fork_entrypoint))
161
+ end
162
+
163
+ def fork_entrypoint(io)
164
+ # Ensure we can work with our forks async...
165
+ Fiber.set_scheduler(nil) if Fiber.respond_to? :set_scheduler
166
+
167
+ @in_fork = io
168
+
169
+ until io.eof?
170
+ $0 = 'opal/builder: idle'
171
+
172
+ type, *args = *io.recv
173
+ case type
174
+ when :compile
175
+ rel_path, req, autoloads, options = *args
176
+ $0 = "opal/builder: #{req}"
177
+ begin
178
+ asset = builder.process_require_threadsafely(req, autoloads, options)
179
+ io.send(:new_asset, asset)
180
+ rescue Builder::MissingRequire => error
181
+ io.send(:missing_require_exception, rel_path, error)
182
+ end
183
+ when :close
184
+ io.goodbye
185
+ break
186
+ end
187
+ end
188
+ rescue Errno::EPIPE
189
+ exit!
190
+ end
191
+
192
+ def prefork_reactor(rel_path, requires, autoloads, options)
193
+ prefork
194
+
195
+ first = rel_path
196
+ queue = requires.map { |i| [rel_path, i, autoloads, options] }
197
+
198
+ awaiting = 0
199
+ built = 0
200
+
201
+ $stderr.print "\r\e[K" if $stderr.tty?
202
+
203
+ loop do
204
+ events, idles = @forks.get_events(queue.length)
205
+
206
+ idles.each do |io|
207
+ break if queue.empty?
208
+
209
+ rel_path, req, autoloads, options = *queue.pop
210
+
211
+ next if builder.already_processed.include?(req)
212
+ awaiting += 1
213
+ builder.already_processed << req
214
+ io.send(:compile, rel_path, req, autoloads, options)
215
+ end
216
+
217
+ events.each do |io, type, *args|
218
+ case type
219
+ when :new_requires
220
+ rel_path, requires, autoloads, options = *args
221
+ requires.each do |i|
222
+ queue << [rel_path, i, autoloads, options]
223
+ end
224
+ when :new_asset
225
+ asset, = *args
226
+ if !asset
227
+ # Do nothing, we received a nil which is expected.
228
+ elsif asset.filename == 'corelib/runtime.js'
229
+ # Opal runtime should go first... the rest can go their way.
230
+ builder.processed.unshift(asset)
231
+ else
232
+ builder.processed << asset
233
+ end
234
+ built += 1
235
+ awaiting -= 1
236
+ when :missing_require_exception
237
+ rel_path, error = *args
238
+ raise error, "A file required by #{rel_path.inspect} wasn't found.\n#{error.message}", error.backtrace
239
+ when :exception
240
+ error, = *args
241
+ raise error
242
+ when :close
243
+ io.goodbye
244
+ end
245
+ end
246
+
247
+ if $stderr.tty?
248
+ percent = (100.0 * built / (awaiting + built)).round(1)
249
+ str = format("[opal/builder] Building %<first>s... (%<percent>4.3g%%)\r", first: first, percent: percent)
250
+ $stderr.print str
251
+ end
252
+
253
+ break if awaiting == 0 && queue.empty?
254
+ end
255
+ ensure
256
+ $stderr.print "\r\e[K\r" if $stderr.tty?
257
+ @forks.close
258
+ @forks.wait
259
+ end
260
+ end
261
+ end
262
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opal
4
+ class BuilderScheduler
5
+ class Sequential < BuilderScheduler
6
+ def process_requires(rel_path, requires, autoloads, options)
7
+ requires.map { |r| builder.process_require(r, autoloads, options) }
8
+ rescue Builder::MissingRequire => error
9
+ raise error, "A file required by #{rel_path.inspect} wasn't found.\n#{error.message}", error.backtrace
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'opal/os' unless RUBY_ENGINE == 'opal'
4
+
5
+ module Opal
6
+ class BuilderScheduler
7
+ def initialize(builder)
8
+ @builder = builder
9
+ end
10
+
11
+ attr_reader :builder
12
+ end
13
+
14
+ singleton_class.attr_accessor :builder_scheduler
15
+
16
+ if RUBY_ENGINE != 'opal'
17
+ # Windows has a faulty `fork`.
18
+ if OS.windows? || ENV['OPAL_PREFORK_DISABLE']
19
+ require 'opal/builder_scheduler/sequential'
20
+ Opal.builder_scheduler = BuilderScheduler::Sequential
21
+ else
22
+ require 'opal/builder_scheduler/prefork'
23
+ Opal.builder_scheduler = BuilderScheduler::Prefork
24
+ end
25
+ else
26
+ require 'opal/builder_scheduler/sequential'
27
+ Opal.builder_scheduler = BuilderScheduler::Sequential
28
+ end
29
+ end
@@ -19,9 +19,20 @@ module Opal
19
19
 
20
20
  def set(key, data)
21
21
  file = cache_filename_for(key)
22
-
23
22
  out = Marshal.dump(data)
24
- out = Zlib.gzip(out, level: 9)
23
+
24
+ # Sometimes `Zlib::BufError` gets raised, unsure why, makes no sense, possibly
25
+ # some race condition (see https://github.com/ruby/zlib/issues/49).
26
+ # Limit the number of retries to avoid infinite loops.
27
+ retries = 5
28
+ begin
29
+ out = Zlib.gzip(out, level: 9)
30
+ rescue Zlib::BufError
31
+ warn "\n[Opal]: Zlib::BufError; retrying (#{retries} retries left)"
32
+ retries -= 1
33
+ retry if retries > 0
34
+ end
35
+
25
36
  File.binwrite(file, out)
26
37
  end
27
38
 
data/lib/opal/cli.rb CHANGED
@@ -48,14 +48,12 @@ module Opal
48
48
 
49
49
  @requires.unshift('opal') unless options.delete(:skip_opal_require)
50
50
 
51
- @compiler_options = Hash[
52
- *compiler_option_names.map do |option|
53
- key = option.to_sym
54
- next unless options.key? key
55
- value = options.delete(key)
56
- [key, value]
57
- end.compact.flatten
58
- ]
51
+ @compiler_options = compiler_option_names.map do |option|
52
+ key = option.to_sym
53
+ next unless options.key? key
54
+ value = options.delete(key)
55
+ [key, value]
56
+ end.compact.to_h
59
57
 
60
58
  raise ArgumentError, 'no libraries to compile' if @lib_only && @requires.empty?
61
59
  raise ArgumentError, 'no runnable code provided (evals or file)' if @evals.empty? && @file.nil? && !@lib_only
@@ -68,6 +66,16 @@ module Opal
68
66
  return debug_source_map if @debug_source_map
69
67
  return run_repl if @repl
70
68
 
69
+ rbrequires.each { |file| require file }
70
+
71
+ runner = self.runner
72
+
73
+ # Some runners may need to use a dynamic builder, that is,
74
+ # a builder that will try to build the entire package every time
75
+ # a page is loaded - for example a Server runner that needs to
76
+ # rerun if files are changed.
77
+ builder = proc { create_builder }
78
+
71
79
  @exit_status = runner.call(
72
80
  options: runner_options,
73
81
  output: output,
@@ -90,13 +98,7 @@ module Opal
90
98
 
91
99
  attr_reader :exit_status
92
100
 
93
- def builder
94
- @builder ||= create_builder
95
- end
96
-
97
101
  def create_builder
98
- rbrequires.each(&Kernel.method(:require))
99
-
100
102
  builder = Opal::Builder.new(
101
103
  stubs: stubs,
102
104
  compiler_options: compiler_options,
@@ -113,22 +115,22 @@ module Opal
113
115
  gems.each { |gem_name| builder.use_gem gem_name }
114
116
 
115
117
  # --require
116
- requires.each { |required| builder.build(required) }
118
+ requires.each { |required| builder.build(required, requirable: true, load: true) }
117
119
 
118
120
  # --preload
119
121
  preload.each { |path| builder.build_require(path) }
120
122
 
121
123
  # --verbose
122
- builder.build_str '$VERBOSE = true', '(flags)' if verbose
124
+ builder.build_str '$VERBOSE = true', '(flags)', no_export: true if verbose
123
125
 
124
126
  # --debug
125
- builder.build_str '$DEBUG = true', '(flags)' if debug
127
+ builder.build_str '$DEBUG = true', '(flags)', no_export: true if debug
126
128
 
127
129
  # --eval / stdin / file
128
130
  evals_or_file { |source, filename| builder.build_str(source, filename) }
129
131
 
130
132
  # --no-exit
131
- builder.build_str '::Kernel.exit', '(exit)' unless no_exit
133
+ builder.build_str '::Kernel.exit', '(exit)', no_export: true unless no_exit
132
134
 
133
135
  builder
134
136
  end
@@ -166,6 +168,7 @@ module Opal
166
168
  irb_enabled
167
169
  inline_operators
168
170
  enable_source_location
171
+ enable_file_source_embed
169
172
  use_strict
170
173
  parse_comments
171
174
  esm
@@ -181,7 +184,21 @@ module Opal
181
184
  if evals.any?
182
185
  yield evals.join("\n"), '-e'
183
186
  elsif file && (filename != '-' || evals.empty?)
184
- yield file.read, filename
187
+ return @content if @content
188
+
189
+ if file.tty?
190
+ save = true
191
+ else
192
+ begin
193
+ file.rewind
194
+ rescue Errno::ESPIPE
195
+ save = true
196
+ end
197
+ end
198
+
199
+ content = yield(file.read, filename)
200
+ @content = content if save
201
+ content
185
202
  end
186
203
  end
187
204
  end
@@ -181,6 +181,10 @@ module Opal
181
181
  options[:enable_source_location] = true
182
182
  end
183
183
 
184
+ on('--enable-file-source-embed', 'Embeds file sources to be accessed by applications.') do
185
+ options[:enable_file_source_embed] = true
186
+ end
187
+
184
188
  on('--use-strict', 'Enables JavaScript\'s strict mode (i.e., adds \'use strict\'; statement)') do
185
189
  options[:use_strict] = true
186
190
  end
@@ -5,6 +5,7 @@ require 'socket'
5
5
  require 'timeout'
6
6
  require 'tmpdir'
7
7
  require 'rbconfig'
8
+ require 'opal/os'
8
9
 
9
10
  module Opal
10
11
  module CliRunners
@@ -20,7 +21,7 @@ module Opal
20
21
  end
21
22
 
22
23
  def initialize(data)
23
- builder = data[:builder]
24
+ builder = data[:builder].call
24
25
  options = data[:options]
25
26
  argv = data[:argv]
26
27
 
@@ -65,28 +66,32 @@ module Opal
65
66
  def prepare_files_in(dir)
66
67
  js = builder.to_s
67
68
  map = builder.source_map.to_json
68
- stack = File.read("#{__dir__}/source-map-support-browser.js")
69
+ stack = File.binread("#{__dir__}/source-map-support-browser.js")
70
+
71
+ ext = builder.output_extension
72
+ module_type = ' type="module"' if builder.esm?
69
73
 
70
74
  # Chrome can't handle huge data passed to `addScriptToEvaluateOnLoad`
71
75
  # https://groups.google.com/a/chromium.org/forum/#!topic/chromium-discuss/U5qyeX_ydBo
72
76
  # The only way is to create temporary files and pass them to chrome.
73
- File.write("#{dir}/index.js", js)
74
- File.write("#{dir}/source-map-support.js", stack)
75
- File.write("#{dir}/index.html", <<~HTML)
77
+ File.binwrite("#{dir}/index.#{ext}", js)
78
+ File.binwrite("#{dir}/source-map-support.js", stack)
79
+ File.binwrite("#{dir}/index.html", <<~HTML)
76
80
  <html><head>
77
81
  <meta charset='utf-8'>
78
82
  <script src='./source-map-support.js'></script>
79
83
  <script>
84
+ window.opalheadlesschrome = true;
80
85
  sourceMapSupport.install({
81
86
  retrieveSourceMap: function(path) {
82
- return path.endsWith('/index.js') ? {
87
+ return path.endsWith('/index.#{ext}') ? {
83
88
  url: './index.map', map: #{map.to_json}
84
89
  } : null;
85
90
  }
86
91
  });
87
92
  </script>
88
93
  </head><body>
89
- <script src='./index.js'></script>
94
+ <script src='./index.#{ext}'#{module_type}></script>
90
95
  </body></html>
91
96
  HTML
92
97
  end
@@ -111,7 +116,7 @@ module Opal
111
116
  raise 'Chrome server can be started only on localhost' if chrome_host != DEFAULT_CHROME_HOST
112
117
 
113
118
  # Disable web security with "--disable-web-security" flag to be able to do XMLHttpRequest (see test_openuri.rb)
114
- chrome_server_cmd = %{#{chrome_executable.shellescape} \
119
+ chrome_server_cmd = %{#{OS.shellescape(chrome_executable)} \
115
120
  --headless \
116
121
  --disable-web-security \
117
122
  --remote-debugging-port=#{chrome_port} \
@@ -119,7 +124,7 @@ module Opal
119
124
 
120
125
  chrome_pid = Process.spawn(chrome_server_cmd)
121
126
 
122
- Timeout.timeout(10) do
127
+ Timeout.timeout(30) do
123
128
  loop do
124
129
  break if chrome_server_running?
125
130
  sleep 0.5
@@ -132,7 +137,7 @@ module Opal
132
137
  puts 'Make sure that you have it installed and that its version is > 59'
133
138
  exit(1)
134
139
  ensure
135
- if Gem.win_platform? && chrome_pid
140
+ if OS.windows? && chrome_pid
136
141
  Process.kill('KILL', chrome_pid) unless system("taskkill /f /t /pid #{chrome_pid} >NUL 2>NUL")
137
142
  elsif chrome_pid
138
143
  Process.kill('HUP', chrome_pid)
@@ -149,8 +154,7 @@ module Opal
149
154
 
150
155
  def chrome_executable
151
156
  ENV['GOOGLE_CHROME_BINARY'] ||
152
- case RbConfig::CONFIG['host_os']
153
- when /bccwin|cygwin|djgpp|mingw|mswin|wince/
157
+ if OS.windows?
154
158
  [
155
159
  'C:/Program Files/Google/Chrome Dev/Application/chrome.exe',
156
160
  'C:/Program Files/Google/Chrome/Application/chrome.exe'
@@ -158,7 +162,7 @@ module Opal
158
162
  next unless File.exist? path
159
163
  return path
160
164
  end
161
- when /darwin|mac os/
165
+ elsif OS.macos?
162
166
  '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
163
167
  else
164
168
  %w[
@@ -106,17 +106,34 @@ CDP(options, function(client) {
106
106
  else
107
107
  `Page.handleJavaScriptDialog({accept: false})`
108
108
  end
109
+ elsif `dialog.type` == 'alert' && `dialog.message` == 'opalheadlesschromeexit'
110
+ # A special case of an alert with a magic string "opalheadlesschromeexit".
111
+ # This denotes that `Kernel#exit` has been called. We would have rather used
112
+ # an exception here, but they don't bubble sometimes.
113
+ %x{
114
+ Page.handleJavaScriptDialog({accept: true});
115
+ Runtime.evaluate({ expression: "window.OPAL_EXIT_CODE" }).then(function(output) {
116
+ client.close();
117
+ if (typeof(output.result) !== "undefined" && output.result.type === "number") {
118
+ process.exit(output.result.value);
119
+ } else {
120
+ process.exit(0);
121
+ }
122
+ });
123
+ }
109
124
  end
110
125
  }
111
126
  });
112
127
 
113
128
  Page.loadEventFired(() => {
114
129
  Runtime.evaluate({ expression: "window.OPAL_EXIT_CODE" }).then(function(output) {
115
- client.close();
116
-
117
130
  if (typeof(output.result) !== "undefined" && output.result.type === "number") {
131
+ client.close();
118
132
  process.exit(output.result.value);
133
+ } else if (typeof(output.result) !== "undefined" && output.result.type === "string" && output.result.value === "noexit") {
134
+ // do nothing, we have headless chrome support enabled and there are most probably async events awaiting
119
135
  } else {
136
+ client.close();
120
137
  process.exit(0);
121
138
  }
122
139
  })
@@ -5,7 +5,7 @@ require 'opal/paths'
5
5
  # The compiler runner will just output the compiled JavaScript
6
6
  Opal::CliRunners::Compiler = ->(data) {
7
7
  options = data[:options] || {}
8
- builder = data.fetch(:builder)
8
+ builder = data.fetch(:builder).call
9
9
  map_file = options[:map_file]
10
10
  output = data.fetch(:output)
11
11
 
@@ -10,10 +10,12 @@ module Opal
10
10
  class Gjs
11
11
  def self.call(data)
12
12
  exe = ENV['GJS_PATH'] || 'gjs'
13
+ builder = data[:builder].call
13
14
 
14
15
  opts = Shellwords.shellwords(ENV['GJS_OPTS'] || '')
16
+ opts.unshift('-m') if builder.esm?
15
17
 
16
- SystemRunner.call(data) do |tempfile|
18
+ SystemRunner.call(data.merge(builder: -> { builder })) do |tempfile|
17
19
  [exe, *opts, tempfile.path, *data[:argv]]
18
20
  end
19
21
  rescue Errno::ENOENT
@@ -9,11 +9,15 @@ module Opal
9
9
  def self.call(data)
10
10
  ::MiniRacer::Platform.set_flags! :harmony
11
11
 
12
- builder = data.fetch(:builder)
12
+ builder = data.fetch(:builder).call
13
13
  output = data.fetch(:output)
14
14
  # TODO: pass it
15
15
  argv = data.fetch(:argv)
16
16
 
17
+ # MiniRacer doesn't like to fork. Let's build Opal first
18
+ # in a forked environment.
19
+ code = builder.to_s + "\n" + builder.source_map.to_data_uri_comment
20
+
17
21
  v8 = ::MiniRacer::Context.new
18
22
  v8.attach('prompt', ->(_msg = '') { $stdin.gets&.chomp })
19
23
  v8.attach('console.log', ->(i) { output.print(i); output.flush })
@@ -22,8 +26,6 @@ module Opal
22
26
  v8.attach('opalminiracer.exit', ->(status) { Kernel.exit(status) })
23
27
  v8.attach('opalminiracer.argv', argv)
24
28
 
25
- code = builder.to_s + "\n" + builder.source_map.to_data_uri_comment
26
-
27
29
  v8.eval(code)
28
30
  end
29
31