opal 1.5.0 → 1.6.0.alpha1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (135) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/build.yml +5 -1
  3. data/CHANGELOG.md +41 -28
  4. data/HACKING.md +23 -0
  5. data/UNRELEASED.md +43 -0
  6. data/benchmark/run.rb +1 -0
  7. data/docs/compiled_ruby.md +8 -0
  8. data/docs/compiler.md +1 -1
  9. data/docs/compiler_directives.md +1 -1
  10. data/docs/getting_started.md +17 -0
  11. data/docs/headless_chrome.md +1 -1
  12. data/docs/index.md +123 -0
  13. data/docs/templates.md +37 -37
  14. data/docs/unsupported_features.md +0 -4
  15. data/lib/opal/builder.rb +59 -39
  16. data/lib/opal/builder_scheduler/prefork.rb +259 -0
  17. data/lib/opal/builder_scheduler/sequential.rb +13 -0
  18. data/lib/opal/builder_scheduler.rb +29 -0
  19. data/lib/opal/cache/file_cache.rb +5 -0
  20. data/lib/opal/cli.rb +22 -18
  21. data/lib/opal/cli_options.rb +4 -0
  22. data/lib/opal/cli_runners/chrome.rb +13 -9
  23. data/lib/opal/cli_runners/chrome_cdp_interface.rb +19 -2
  24. data/lib/opal/cli_runners/compiler.rb +1 -1
  25. data/lib/opal/cli_runners/gjs.rb +3 -1
  26. data/lib/opal/cli_runners/mini_racer.rb +5 -3
  27. data/lib/opal/cli_runners/nodejs.rb +3 -3
  28. data/lib/opal/cli_runners/server.rb +13 -28
  29. data/lib/opal/cli_runners/system_runner.rb +5 -3
  30. data/lib/opal/cli_runners.rb +7 -6
  31. data/lib/opal/compiler.rb +25 -2
  32. data/lib/opal/config.rb +10 -0
  33. data/lib/opal/nodes/args/ensure_kwargs_are_kwargs.rb +2 -6
  34. data/lib/opal/nodes/args/extract_kwarg.rb +3 -4
  35. data/lib/opal/nodes/args/extract_kwargs.rb +3 -1
  36. data/lib/opal/nodes/args/extract_kwoptarg.rb +1 -1
  37. data/lib/opal/nodes/args/extract_kwrestarg.rb +4 -1
  38. data/lib/opal/nodes/args/extract_optarg.rb +1 -1
  39. data/lib/opal/nodes/args/extract_post_arg.rb +1 -1
  40. data/lib/opal/nodes/args/extract_post_optarg.rb +1 -1
  41. data/lib/opal/nodes/args/extract_restarg.rb +2 -2
  42. data/lib/opal/nodes/args/initialize_iterarg.rb +1 -1
  43. data/lib/opal/nodes/args/initialize_shadowarg.rb +1 -1
  44. data/lib/opal/nodes/args/prepare_post_args.rb +4 -2
  45. data/lib/opal/nodes/base.rb +14 -3
  46. data/lib/opal/nodes/call.rb +10 -14
  47. data/lib/opal/nodes/class.rb +3 -1
  48. data/lib/opal/nodes/closure.rb +250 -0
  49. data/lib/opal/nodes/def.rb +7 -11
  50. data/lib/opal/nodes/definitions.rb +4 -2
  51. data/lib/opal/nodes/if.rb +12 -2
  52. data/lib/opal/nodes/iter.rb +11 -17
  53. data/lib/opal/nodes/logic.rb +15 -63
  54. data/lib/opal/nodes/module.rb +3 -1
  55. data/lib/opal/nodes/rescue.rb +23 -15
  56. data/lib/opal/nodes/scope.rb +7 -1
  57. data/lib/opal/nodes/top.rb +27 -4
  58. data/lib/opal/nodes/while.rb +42 -26
  59. data/lib/opal/nodes.rb +1 -0
  60. data/lib/opal/os.rb +59 -0
  61. data/lib/opal/rewriter.rb +2 -0
  62. data/lib/opal/rewriters/returnable_logic.rb +14 -0
  63. data/lib/opal/rewriters/thrower_finder.rb +90 -0
  64. data/lib/opal/simple_server.rb +12 -6
  65. data/lib/opal/source_map/file.rb +1 -1
  66. data/lib/opal/source_map/map.rb +9 -1
  67. data/lib/opal/util.rb +1 -1
  68. data/lib/opal/version.rb +1 -1
  69. data/opal/corelib/array.rb +68 -3
  70. data/opal/corelib/basic_object.rb +1 -0
  71. data/opal/corelib/comparable.rb +1 -1
  72. data/opal/corelib/complex.rb +1 -0
  73. data/opal/corelib/constants.rb +2 -2
  74. data/opal/corelib/enumerable.rb +4 -2
  75. data/opal/corelib/enumerator/chain.rb +4 -0
  76. data/opal/corelib/enumerator/generator.rb +5 -3
  77. data/opal/corelib/enumerator/lazy.rb +3 -1
  78. data/opal/corelib/enumerator/yielder.rb +2 -4
  79. data/opal/corelib/enumerator.rb +3 -1
  80. data/opal/corelib/error/errno.rb +2 -1
  81. data/opal/corelib/error.rb +13 -2
  82. data/opal/corelib/hash.rb +40 -2
  83. data/opal/corelib/kernel.rb +56 -5
  84. data/opal/corelib/module.rb +60 -4
  85. data/opal/corelib/proc.rb +8 -5
  86. data/opal/corelib/rational.rb +1 -0
  87. data/opal/corelib/regexp.rb +15 -1
  88. data/opal/corelib/runtime.js +307 -238
  89. data/opal/corelib/string/encoding.rb +0 -6
  90. data/opal/corelib/string.rb +28 -7
  91. data/opal/corelib/time.rb +18 -12
  92. data/opal/corelib/unsupported.rb +2 -14
  93. data/spec/filters/bugs/delegate.rb +11 -0
  94. data/spec/filters/bugs/kernel.rb +1 -3
  95. data/spec/filters/bugs/language.rb +3 -23
  96. data/spec/filters/bugs/method.rb +0 -1
  97. data/spec/filters/bugs/module.rb +0 -3
  98. data/spec/filters/bugs/proc.rb +0 -3
  99. data/spec/filters/bugs/set.rb +4 -16
  100. data/spec/filters/bugs/stringscanner.rb +0 -1
  101. data/spec/filters/bugs/unboundmethod.rb +0 -2
  102. data/spec/filters/unsupported/array.rb +0 -58
  103. data/spec/filters/unsupported/freeze.rb +8 -192
  104. data/spec/filters/unsupported/hash.rb +0 -25
  105. data/spec/filters/unsupported/kernel.rb +0 -1
  106. data/spec/filters/unsupported/privacy.rb +17 -0
  107. data/spec/lib/builder_spec.rb +14 -0
  108. data/spec/lib/cli_runners/server_spec.rb +2 -3
  109. data/spec/lib/cli_spec.rb +1 -1
  110. data/spec/lib/compiler_spec.rb +1 -1
  111. data/spec/opal/core/language/if_spec.rb +13 -0
  112. data/spec/opal/core/module_spec.rb +8 -0
  113. data/spec/ruby_specs +2 -1
  114. data/spec/spec_helper.rb +4 -0
  115. data/stdlib/await.rb +44 -7
  116. data/stdlib/delegate.rb +427 -6
  117. data/stdlib/headless_chrome.rb +6 -2
  118. data/stdlib/nodejs/file.rb +2 -1
  119. data/stdlib/opal-parser.rb +1 -1
  120. data/stdlib/opal-platform.rb +1 -1
  121. data/stdlib/opal-replutils.rb +5 -3
  122. data/stdlib/promise.rb +3 -0
  123. data/stdlib/rbconfig.rb +4 -1
  124. data/stdlib/ruby2_keywords.rb +60 -0
  125. data/stdlib/set.rb +21 -0
  126. data/stdlib/strscan.rb +27 -49
  127. data/tasks/performance.rake +41 -35
  128. data/tasks/releasing.rake +1 -0
  129. data/tasks/testing/mspec_special_calls.rb +1 -0
  130. data/tasks/testing.rake +13 -8
  131. data/test/nodejs/test_await.rb +39 -1
  132. metadata +27 -14
  133. data/docs/faq.md +0 -17
  134. data/lib/opal/rewriters/break_finder.rb +0 -36
  135. data/spec/opal/core/kernel/freeze_spec.rb +0 -15
@@ -0,0 +1,259 @@
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
+ @in_fork = io
165
+
166
+ until io.eof?
167
+ $0 = 'opal/builder: idle'
168
+
169
+ type, *args = *io.recv
170
+ case type
171
+ when :compile
172
+ rel_path, req, autoloads, options = *args
173
+ $0 = "opal/builder: #{req}"
174
+ begin
175
+ asset = builder.process_require_threadsafely(req, autoloads, options)
176
+ io.send(:new_asset, asset)
177
+ rescue Builder::MissingRequire => error
178
+ io.send(:missing_require_exception, rel_path, error)
179
+ end
180
+ when :close
181
+ io.goodbye
182
+ break
183
+ end
184
+ end
185
+ rescue Errno::EPIPE
186
+ exit!
187
+ end
188
+
189
+ def prefork_reactor(rel_path, requires, autoloads, options)
190
+ prefork
191
+
192
+ first = rel_path
193
+ queue = requires.map { |i| [rel_path, i, autoloads, options] }
194
+
195
+ awaiting = 0
196
+ built = 0
197
+
198
+ $stderr.print "\r\e[K" if $stderr.tty?
199
+
200
+ loop do
201
+ events, idles = @forks.get_events(queue.length)
202
+
203
+ idles.each do |io|
204
+ break if queue.empty?
205
+
206
+ rel_path, req, autoloads, options = *queue.pop
207
+
208
+ next if builder.already_processed.include?(req)
209
+ awaiting += 1
210
+ builder.already_processed << req
211
+ io.send(:compile, rel_path, req, autoloads, options)
212
+ end
213
+
214
+ events.each do |io, type, *args|
215
+ case type
216
+ when :new_requires
217
+ rel_path, requires, autoloads, options = *args
218
+ requires.each do |i|
219
+ queue << [rel_path, i, autoloads, options]
220
+ end
221
+ when :new_asset
222
+ asset, = *args
223
+ if !asset
224
+ # Do nothing, we received a nil which is expected.
225
+ elsif asset.filename == 'corelib/runtime.js'
226
+ # Opal runtime should go first... the rest can go their way.
227
+ builder.processed.unshift(asset)
228
+ else
229
+ builder.processed << asset
230
+ end
231
+ built += 1
232
+ awaiting -= 1
233
+ when :missing_require_exception
234
+ rel_path, error = *args
235
+ raise error, "A file required by #{rel_path.inspect} wasn't found.\n#{error.message}", error.backtrace
236
+ when :exception
237
+ error, = *args
238
+ raise error
239
+ when :close
240
+ io.goodbye
241
+ end
242
+ end
243
+
244
+ if $stderr.tty?
245
+ percent = (100.0 * built / (awaiting + built)).round(1)
246
+ str = format("[opal/builder] Building %<first>s... (%<percent>4.3g%%)\r", first: first, percent: percent)
247
+ $stderr.print str
248
+ end
249
+
250
+ break if awaiting == 0 && queue.empty?
251
+ end
252
+ ensure
253
+ $stderr.print "\r\e[K\r" if $stderr.tty?
254
+ @forks.close
255
+ @forks.wait
256
+ end
257
+ end
258
+ end
259
+ 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
@@ -23,6 +23,11 @@ module Opal
23
23
  out = Marshal.dump(data)
24
24
  out = Zlib.gzip(out, level: 9)
25
25
  File.binwrite(file, out)
26
+ rescue Zlib::BufError
27
+ # This sometimes happens, unsure why, makes no sense, possibly
28
+ # some race condition
29
+ warn '[Opal]: Zlib::BufError; retrying'
30
+ retry
26
31
  end
27
32
 
28
33
  def get(key)
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,6 +184,7 @@ module Opal
181
184
  if evals.any?
182
185
  yield evals.join("\n"), '-e'
183
186
  elsif file && (filename != '-' || evals.empty?)
187
+ file.rewind
184
188
  yield file.read, filename
185
189
  end
186
190
  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
 
@@ -67,26 +68,30 @@ module Opal
67
68
  map = builder.source_map.to_json
68
69
  stack = File.read("#{__dir__}/source-map-support-browser.js")
69
70
 
71
+ ext = builder.output_extension
72
+ module_type = ' type="module"' if builder.esm?
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)
77
+ File.write("#{dir}/index.#{ext}", js)
74
78
  File.write("#{dir}/source-map-support.js", stack)
75
79
  File.write("#{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} \
@@ -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 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
 
@@ -3,6 +3,7 @@
3
3
  require 'shellwords'
4
4
  require 'opal/paths'
5
5
  require 'opal/cli_runners/system_runner'
6
+ require 'opal/os'
6
7
 
7
8
  module Opal
8
9
  module CliRunners
@@ -39,10 +40,9 @@ module Opal
39
40
 
40
41
  # Ensure stdlib node_modules is among NODE_PATHs
41
42
  def self.node_modules
42
- npsep = Gem.win_platform? ? ';' : ':'
43
- ENV['NODE_PATH'].to_s.split(npsep).tap do |paths|
43
+ ENV['NODE_PATH'].to_s.split(OS.env_sep).tap do |paths|
44
44
  paths << NODE_PATH unless paths.include? NODE_PATH
45
- end.join(npsep)
45
+ end.join(OS.env_sep)
46
46
  end
47
47
 
48
48
  class MissingNodeJS < RunnerError