Skip to content

Commit 8fd3180

Browse files
committed
Load external Ruby scripts from the browser with require_relative.
1 parent 097b7ca commit 8fd3180

File tree

8 files changed

+139
-0
lines changed

8 files changed

+139
-0
lines changed

ext/js/lib/js.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
require_relative "js/hash.rb"
33
require_relative "js/array.rb"
44
require_relative "js/nil_class.rb"
5+
require_relative "js/browser.rb"
56

67
# The JS module provides a way to interact with JavaScript from Ruby.
78
#

ext/js/lib/js/browser.rb

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
require "singleton"
2+
require_relative "./url_resolver"
3+
4+
module JS
5+
ScriptLocation = Data.define(:url, :filename)
6+
7+
class Browser
8+
include Singleton
9+
10+
def initialize
11+
set_base_url
12+
end
13+
14+
def require_relative(relative_feature)
15+
location = get_location(relative_feature)
16+
response = JS.global.fetch(location.url).await
17+
18+
if response[:status].to_i == 200
19+
code = response.text().await.to_s
20+
eval_code(code, location)
21+
else
22+
raise LoadError.new "cannot load such file -- #{relative_feature}"
23+
end
24+
end
25+
26+
private
27+
28+
def set_base_url
29+
base_url = JS.global[:URL].new(JS.global[:location][:href])
30+
@resolver = URLResolver.new(base_url)
31+
end
32+
33+
def get_location(relative_feature)
34+
filename = filename_from(relative_feature)
35+
url = resolver.resolve(filename)
36+
ScriptLocation.new(url, filename)
37+
end
38+
39+
# Evaluate the given Ruby code with the given location and save the URL to the stack.
40+
def eval_code(code, location)
41+
begin
42+
resolver.push(location.url)
43+
Kernel.eval(code, ::Object::TOPLEVEL_BINDING, location.filename)
44+
ensure
45+
resolver.pop
46+
end
47+
end
48+
49+
def filename_from(relative_feature)
50+
relative_feature.end_with?(".rb") ? relative_feature : "#{relative_feature}.rb"
51+
end
52+
53+
attr_reader :resolver
54+
end
55+
end

ext/js/lib/js/url_resolver.rb

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
module JS
2+
# When require_relative is called within a running Ruby script,
3+
# the URL is resolved from a relative file path based on the URL of the running Ruby script.
4+
# It uses a stack to store URLs of running Ruby Script.
5+
# Push the URL onto the stack before executing the new script.
6+
# Then pop it when the script has finished executing.
7+
class URLResolver
8+
def initialize(base_url)
9+
@url_stack = [base_url]
10+
end
11+
12+
# Return a URL object of JavaScript.
13+
def resolve(relative_filepath)
14+
JS.global[:URL].new relative_filepath, @url_stack.last
15+
end
16+
17+
def push(url)
18+
@url_stack.push url
19+
end
20+
21+
def pop()
22+
@url_stack.pop
23+
end
24+
end
25+
end
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
class Greeting
2+
def say
3+
puts "Hello, world!"
4+
end
5+
end
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<html>
2+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/browser.script.iife.js"></script>
3+
<script type="text/ruby" data-eval="async">
4+
require_relative 'main'
5+
</script>
6+
</html>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
require_relative "greeting"
2+
3+
Greeting.new.say

packages/npm-packages/ruby-wasm-wasi/src/browser.script.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { RubyVM } from ".";
12
import { DefaultRubyVM } from "./browser";
23

34
export const main = async (pkg: { name: string; version: string }) => {
@@ -12,6 +13,8 @@ export const main = async (pkg: { name: string; version: string }) => {
1213

1314
globalThis.rubyVM = vm;
1415

16+
patchRequireRelative(vm);
17+
1518
// Wait for the text/ruby script tag to be read.
1619
// It may take some time to read ruby+stdlib.wasm
1720
// and DOMContentLoaded has already been fired.
@@ -79,3 +82,27 @@ const loadScriptAsync = async (
7982

8083
return Promise.resolve({ scriptContent: tag.innerHTML, evalStyle });
8184
};
85+
86+
const patchRequireRelative = (vm: RubyVM) => {
87+
const patch = `
88+
require 'js'
89+
90+
module Kernel
91+
alias original_require_relative require_relative
92+
93+
# The require_relative may be used in the embedded Gem.
94+
# First try to load from the built-in filesystem, and if that fails,
95+
# load from the URL.
96+
def require_relative(path)
97+
caller_path = caller_locations(1, 1).first.absolute_path || ''
98+
dir = File.dirname(caller_path)
99+
file = File.absolute_path(path, dir)
100+
101+
original_require_relative(file)
102+
rescue LoadError
103+
::JS::Browser.instance.require_relative(path)
104+
end
105+
end
106+
`;
107+
vm.eval(patch);
108+
};

packages/npm-packages/ruby-wasm-wasi/test-e2e/examples/examples.spec.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,20 @@ test("script-src/index.html is healthy", async ({ page }) => {
5858
await page.waitForEvent("console");
5959
}
6060
});
61+
62+
test("require_relative/index.html is healthy", async ({ page }) => {
63+
// Add a listener to detect errors in the page
64+
page.on("pageerror", (error) => {
65+
console.log(`page error occurs: ${error.message}`);
66+
});
67+
68+
const messages: string[] = [];
69+
page.on("console", (msg) => messages.push(msg.text()));
70+
await page.goto("/require_relative/index.html");
71+
72+
await waitForRubyVM(page);
73+
const expected = "Hello, world!\n";
74+
while (messages[messages.length - 1] != expected) {
75+
await page.waitForEvent("console");
76+
}
77+
});

0 commit comments

Comments
 (0)