lib/opal/repl.rb
# frozen_string_literal: true
require 'opal'
require 'securerandom'
require 'stringio'
require 'fileutils'
require 'rbconfig'
module Opal
class REPL
HISTORY_PATH = File.expand_path('~/.opal-repl-history')
attr_accessor :colorize
def initialize
@argv = []
@colorize = true
begin
require 'readline'
rescue LoadError
abort 'opal-repl depends on readline, which is not currently available'
end
begin
FileUtils.touch(HISTORY_PATH)
rescue
nil
end
@history = File.exist?(HISTORY_PATH)
end
def run(argv = [])
@argv = argv
savepoint = save_tty
load_opal
load_history
run_input_loop
ensure
dump_history
restore_tty(savepoint)
end
def load_opal
runner = @argv.reject { |i| i == '--repl' }
runner += ['-e', 'require "opal/repl_js"']
runner = [RbConfig.ruby, "#{__dir__}/../../exe/opal"] + runner
@pipe = IO.popen(runner, 'r+',
# What I try to achieve here: let the runner ignore
# interrupts. Those should be handled by a supervisor.
pgroup: true,
new_pgroup: true,
)
end
def run_input_loop
while (line = readline)
eval_ruby(line)
end
rescue Interrupt
@incomplete = nil
retry
ensure
finish
end
def finish
@pipe.close
rescue
nil
end
def eval_ruby(code)
builder = Opal::Builder.new
silencer = Silencer.new
code = "#{@incomplete}#{code}"
if code.start_with? 'ls '
eval_code = code[3..-1]
mode = :ls
elsif code == 'ls'
eval_code = 'self'
mode = :ls
elsif code.start_with? 'show '
eval_code = code[5..-1]
mode = :show
else
eval_code = code
mode = :inspect
end
begin
silencer.silence do
builder.build_str(eval_code, '(irb)', irb: true, const_missing: true)
end
@incomplete = nil
rescue Opal::SyntaxError => e
if LINEBREAKS.include?(e.message)
@incomplete = "#{code}\n"
else
@incomplete = nil
if silencer.warnings.empty?
warn e.full_message
else
# Most likely a parser error
warn silencer.warnings
end
end
return
end
builder.processed[0...-1].each { |js_code| eval_js(:silent, js_code.to_s) }
last_processed_file = builder.processed.last.to_s
if mode == :show
puts last_processed_file
return
end
eval_js(mode, last_processed_file)
rescue Interrupt, SystemExit => e
raise e
rescue Exception => e # rubocop:disable Lint/RescueException
puts e.full_message(highlight: true)
end
private
LINEBREAKS = [
'unexpected token $end',
'unterminated string meets end of file'
].freeze
class Silencer
def initialize
@stderr = $stderr
end
def silence
@collector = StringIO.new
$stderr = @collector
yield
ensure
$stderr = @stderr
end
def warnings
@collector.string
end
end
def eval_js(mode, code)
obj = { mode: mode, code: code, colors: colorize }.to_json
@pipe.puts obj
while (line = @pipe.readline)
break if line.chomp == '<<<ready>>>'
puts line
end
rescue Interrupt => e
# A child stopped responding... let's create a new one
warn "* Killing #{@pipe.pid}"
Process.kill('-KILL', @pipe.pid)
load_opal
raise e
rescue EOFError, Errno::EPIPE
exit $?.nil? ? 0 : $?.exitstatus
end
def readline
prompt = @incomplete ? '.. ' : '>> '
Readline.readline prompt, true
end
def load_history
return unless @history
File.read(HISTORY_PATH).lines.each { |line| Readline::HISTORY.push line.strip }
end
def dump_history
return unless @history
length = Readline::HISTORY.size > 1000 ? 1000 : Readline::HISTORY.size
File.write(HISTORY_PATH, Readline::HISTORY.to_a[-length..-1].join("\n"))
end
# How do we support Win32?
def save_tty
%x{stty -g}.chomp
rescue
nil
end
def restore_tty(savepoint)
system('stty', savepoint)
rescue
nil
end
end
end