Skip to content

Commit

Permalink
Add CLI support for ESM, at least for Chrome, NodeJS, QuickJS and GJS
Browse files Browse the repository at this point in the history
* This also fixes a bug which caused CLI options `-r` to not be tracked
  by loaded_features
* This fixes a bug which caused CLI-generated ESM files to have a double
  default export
  • Loading branch information
hmdne committed Sep 17, 2022
1 parent 55d3ef0 commit 1353877
Show file tree
Hide file tree
Showing 8 changed files with 68 additions and 17 deletions.
19 changes: 18 additions & 1 deletion lib/opal/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,10 @@ def build_str(source, rel_path, options = {})
rel_path = expand_ext(rel_path)
asset = processor_for(source, rel_path, abs_path, false, options)
requires = preload + asset.requires + tree_requires(asset, abs_path)
requires.map { |r| process_require(r, asset.autoloads, options) }
requires.map do |r|
# Don't automatically load modules required by the module
process_require(r, asset.autoloads, options.merge(load: false))
end
processed << asset
self
rescue MissingRequire => error
Expand Down Expand Up @@ -141,6 +144,20 @@ def append_paths(*paths)
attr_accessor :processors, :path_reader, :stubs, :prerequired, :preload,
:compiler_options, :missing_require_severity, :cache

def esm?
@compiler_options[:esm]
end

# Output extension, to be used by runners. At least Node.JS switches
# to ESM mode only if the extension is "mjs"
def output_extension
if esm?
'mjs'
else
'js'
end
end

private

def tree_requires(asset, asset_path)
Expand Down
8 changes: 4 additions & 4 deletions lib/opal/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -113,22 +113,22 @@ def create_builder
gems.each { |gem_name| builder.use_gem gem_name }

# --require
requires.each { |required| builder.build(required) }
requires.each { |required| builder.build(required, requirable: true, load: true) }

# --preload
preload.each { |path| builder.build_require(path) }

# --verbose
builder.build_str '$VERBOSE = true', '(flags)' if verbose
builder.build_str '$VERBOSE = true', '(flags)', no_export: true if verbose

# --debug
builder.build_str '$DEBUG = true', '(flags)' if debug
builder.build_str '$DEBUG = true', '(flags)', no_export: true if debug

# --eval / stdin / file
evals_or_file { |source, filename| builder.build_str(source, filename) }

# --no-exit
builder.build_str '::Kernel.exit', '(exit)' unless no_exit
builder.build_str '::Kernel.exit', '(exit)', no_export: true unless no_exit

builder
end
Expand Down
9 changes: 6 additions & 3 deletions lib/opal/cli_runners/chrome.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,13 @@ def prepare_files_in(dir)
map = builder.source_map.to_json
stack = File.read("#{__dir__}/source-map-support-browser.js")

ext = builder.output_extension
module_type = ' type="module"' if builder.esm?

# Chrome can't handle huge data passed to `addScriptToEvaluateOnLoad`
# https://groups.google.com/a/chromium.org/forum/#!topic/chromium-discuss/U5qyeX_ydBo
# The only way is to create temporary files and pass them to chrome.
File.write("#{dir}/index.js", js)
File.write("#{dir}/index.#{ext}", js)
File.write("#{dir}/source-map-support.js", stack)
File.write("#{dir}/index.html", <<~HTML)
<html><head>
Expand All @@ -79,14 +82,14 @@ def prepare_files_in(dir)
<script>
sourceMapSupport.install({
retrieveSourceMap: function(path) {
return path.endsWith('/index.js') ? {
return path.endsWith('/index.#{ext}') ? {
url: './index.map', map: #{map.to_json}
} : null;
}
});
</script>
</head><body>
<script src='./index.js'></script>
<script src='./index.#{ext}'#{module_type}></script>
</body></html>
HTML
end
Expand Down
1 change: 1 addition & 0 deletions lib/opal/cli_runners/gjs.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ def self.call(data)
exe = ENV['GJS_PATH'] || 'gjs'

opts = Shellwords.shellwords(ENV['GJS_OPTS'] || '')
opts.unshift('-m') if data[:builder].esm?

SystemRunner.call(data) do |tempfile|
[exe, *opts, tempfile.path, *data[:argv]]
Expand Down
6 changes: 4 additions & 2 deletions lib/opal/cli_runners/system_runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@
# Temporary issue with UTF-8, Base64 and source maps
code += "\n" + builder.source_map.to_data_uri_comment unless RUBY_ENGINE == 'opal'

ext = builder.output_extension

tempfile =
if debug
File.new('opal-nodejs-runner.js', 'w')
File.new("opal-nodejs-runner.#{ext}", 'w')
else
Tempfile.new('opal-system-runner-')
Tempfile.new(['opal-system-runner', ".#{ext}"])
end

tempfile.write code
Expand Down
14 changes: 13 additions & 1 deletion lib/opal/compiler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,23 @@ def option_value(name, config)
# Prepare the code for future requires
compiler_option :requirable, default: false, as: :requirable?

# @!method load?
#
# Instantly load a requirable module
compiler_option :load, default: false, as: :load?

# @!method esm?
#
# Wrap compiler result as self contained ES6 module
# Encourage ESM semantics, eg. exporting run result
compiler_option :esm, default: false, as: :esm?

# @!method no_export?
#
# Don't export this compile, even if ESM mode is enabled. We use
# this internally in CLI, so that even if ESM output is desired,
# we would only have one default export.
compiler_option :no_export, default: false, as: :no_export?

# @!method inline_operators?
#
# are operators compiled inline
Expand Down
16 changes: 14 additions & 2 deletions lib/opal/nodes/top.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,14 @@ def compile
end
end

def module_name
Opal::Compiler.module_name(compiler.file).inspect
end

def definition
if compiler.requirable?
unshift "Opal.modules[#{Opal::Compiler.module_name(compiler.file).inspect}] = "
elsif compiler.esm?
unshift "Opal.modules[#{module_name}] = "
elsif compiler.esm? && !compiler.no_export?
unshift 'export default '
end
end
Expand All @@ -85,6 +89,14 @@ def opening
def closing
if compiler.requirable?
line "};\n"

if compiler.load?
# Opal.load normalizes the path, so that we can't
# require absolute paths from CLI. For other cases
# we can expect the module names to be normalized
# already.
line "Opal.load_normalized(#{module_name});"
end
elsif compiler.eval?
line "})(Opal, self);"
else
Expand Down
12 changes: 8 additions & 4 deletions opal/corelib/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -2650,9 +2650,7 @@
}
};

Opal.load = function(path) {
path = Opal.normalize(path);

Opal.load_normalized = function(path) {
Opal.loaded([path]);

var module = Opal.modules[path];
Expand Down Expand Up @@ -2684,14 +2682,20 @@
return true;
};

Opal.load = function(path) {
path = Opal.normalize(path);

return Opal.load_normalized(path);
};

Opal.require = function(path) {
path = Opal.normalize(path);

if (Opal.require_table[path]) {
return false;
}

return Opal.load(path);
return Opal.load_normalized(path);
};


Expand Down

0 comments on commit 1353877

Please sign in to comment.