-
Notifications
You must be signed in to change notification settings - Fork 55
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add JS::RequireRemote to load external Ruby scripts from the browser
Users use JS::RequireRemote#load. The user uses this method to replace the require_relative method. Fix some test codes. - Extract a custom router for integration tests - Split integration test files - Files the body returned by the proxy server
- Loading branch information
Showing
11 changed files
with
347 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
require "singleton" | ||
require "js" | ||
require_relative "./require_remote/url_resolver" | ||
require_relative "./require_remote/response_handler" | ||
|
||
module JS | ||
class RequireRemote | ||
include Singleton | ||
|
||
def initialize | ||
base_url = JS.global[:URL].new(JS.global[:location][:href]) | ||
@resolver = URLResolver.new(base_url) | ||
@handler = ResponseHandler.new(method(:update_current_url)) | ||
end | ||
|
||
# Load the given feature from remote. | ||
def load(relative_feature) | ||
location = @resolver.get_location(relative_feature) | ||
|
||
# Do not load the same URL twice. | ||
return false if @handler.evaluated?(location) | ||
|
||
response = JS.global.fetch(location.url).await | ||
@handler.execute_and_record(response, location) | ||
end | ||
|
||
private | ||
|
||
def update_current_url(url) | ||
@resolver.push(url) | ||
yield | ||
ensure | ||
@resolver.pop | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
module JS | ||
class RequireRemote | ||
# Execute the body of the response and record the URL. | ||
class ResponseHandler | ||
# The around_eval parameter is a block that is executed before and after the body of the response. | ||
# The block is passed the URL of the response. | ||
def initialize(around_eval) | ||
@around_eval = around_eval | ||
end | ||
|
||
# Execute the body of the response and record the URL. | ||
def execute_and_record(response, location) | ||
if response[:status].to_i == 200 | ||
# Check if the redirected URL has already evaluated. | ||
return false if url_evaluated?(response[:url].to_s) | ||
|
||
code = response.text().await.to_s | ||
evaluate(code, location) | ||
|
||
# The redirect that occurred may have been temporary. | ||
# The original URL is not recorded. | ||
# Only the URL after the redirect is recorded. | ||
$LOADED_FEATURES << response[:url].to_s | ||
true | ||
else | ||
raise LoadError.new "cannot load such url -- #{response[:status]} #{location.url}" | ||
end | ||
end | ||
|
||
def evaluated?(location) | ||
url_evaluated?(location.url[:href].to_s) | ||
end | ||
|
||
private | ||
|
||
def url_evaluated?(url) | ||
$LOADED_FEATURES.include?(url) | ||
end | ||
|
||
# Evaluate the given Ruby code with the given location and save the URL to the stack. | ||
def evaluate(code, location) | ||
@around_eval.call(location.url) do | ||
Kernel.eval(code, ::Object::TOPLEVEL_BINDING, location.filename) | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
module JS | ||
class RequireRemote | ||
ScriptLocation = Data.define(:url, :filename) | ||
|
||
# When require_relative is called within a running Ruby script, | ||
# the URL is resolved from a relative file path based on the URL of the running Ruby script. | ||
# It uses a stack to store URLs of running Ruby Script. | ||
# Push the URL onto the stack before executing the new script. | ||
# Then pop it when the script has finished executing. | ||
class URLResolver | ||
def initialize(base_url) | ||
@url_stack = [base_url] | ||
end | ||
|
||
def get_location(relative_feature) | ||
filename = filename_from(relative_feature) | ||
url = resolve(filename) | ||
ScriptLocation.new(url, filename) | ||
end | ||
|
||
def push(url) | ||
@url_stack.push url | ||
end | ||
|
||
def pop() | ||
@url_stack.pop | ||
end | ||
|
||
private | ||
|
||
def filename_from(relative_feature) | ||
if relative_feature.end_with?(".rb") | ||
relative_feature | ||
else | ||
"#{relative_feature}.rb" | ||
end | ||
end | ||
|
||
# Return a URL object of JavaScript. | ||
def resolve(relative_filepath) | ||
JS.global[:URL].new relative_filepath, @url_stack.last | ||
end | ||
end | ||
end | ||
end |
5 changes: 5 additions & 0 deletions
5
packages/npm-packages/ruby-wasm-wasi/example/require_relative/greeting.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
class Greeting | ||
def say | ||
puts "Hello, world!" | ||
end | ||
end |
31 changes: 31 additions & 0 deletions
31
packages/npm-packages/ruby-wasm-wasi/example/require_relative/index.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
<html> | ||
<script src="https://cdn.jsdelivr.net/npm/@ruby/[email protected]/dist/browser.script.iife.js"></script> | ||
<script type="text/ruby" data-eval="async"> | ||
# Patch require_relative to load from remote | ||
require 'js/require_remote' | ||
|
||
module Kernel | ||
alias original_require_relative require_relative | ||
|
||
# The require_relative may be used in the embedded Gem. | ||
# First try to load from the built-in filesystem, and if that fails, | ||
# load from the URL. | ||
def require_relative(path) | ||
caller_path = caller_locations(1, 1).first.absolute_path || '' | ||
dir = File.dirname(caller_path) | ||
file = File.absolute_path(path, dir) | ||
|
||
original_require_relative(file) | ||
rescue LoadError | ||
JS::RequireRemote.instance.load(path) | ||
end | ||
end | ||
|
||
# The above patch does not break the original require_relative | ||
require 'csv' | ||
csv = CSV.new "foo\nbar\n" | ||
|
||
# Load the main script | ||
require_relative 'main' | ||
</script> | ||
</html> |
3 changes: 3 additions & 0 deletions
3
packages/npm-packages/ruby-wasm-wasi/example/require_relative/main.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
require_relative "greeting" | ||
|
||
Greeting.new.say |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
3 changes: 3 additions & 0 deletions
3
packages/npm-packages/ruby-wasm-wasi/test-e2e/integrations/fixtures/error_on_load_twice.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
raise "load twice" if defined?(ALREADY_LOADED) | ||
|
||
ALREADY_LOADED = true |
122 changes: 122 additions & 0 deletions
122
packages/npm-packages/ruby-wasm-wasi/test-e2e/integrations/js-require-remote.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
import fs from "fs"; | ||
import path from "path"; | ||
import { test, expect } from "@playwright/test"; | ||
import { setupDebugLog, setupProxy, resolveBinding } from "../support"; | ||
|
||
if (!process.env.RUBY_NPM_PACKAGE_ROOT) { | ||
test.skip("skip", () => {}); | ||
} else { | ||
test.beforeEach(async ({ context }) => { | ||
setupDebugLog(context); | ||
setupProxy(context, (route, relativePath, mockedPath) => { | ||
if (relativePath.match("fixtures")) { | ||
route.fulfill({ | ||
path: path.join("./test-e2e/integrations", relativePath), | ||
}); | ||
} else if (fs.existsSync(mockedPath)) { | ||
route.fulfill({ | ||
path: mockedPath, | ||
}); | ||
} else { | ||
route.fulfill({ | ||
status: 404, | ||
}); | ||
} | ||
}); | ||
}); | ||
|
||
test.describe("JS::RequireRemote#load", () => { | ||
test("JS::RequireRemote#load returns true", async ({ page }) => { | ||
const resolve = await resolveBinding(page, "checkResolved"); | ||
await page.goto( | ||
"https://cdn.jsdelivr.net/npm/@ruby/head-wasm-wasi@latest/dist/", | ||
); | ||
await page.setContent(` | ||
<script src="browser.script.iife.js"></script> | ||
<script type="text/ruby" data-eval="async"> | ||
require 'js/require_remote' | ||
JS.global.checkResolved JS::RequireRemote.instance.load 'fixtures/error_on_load_twice' | ||
</script> | ||
`); | ||
|
||
expect(await resolve()).toBe(true); | ||
}); | ||
|
||
test("JS::RequireRemote#load returns false when same gem is loaded twice", async ({ | ||
page, | ||
}) => { | ||
const resolve = await resolveBinding(page, "checkResolved"); | ||
await page.goto( | ||
"https://cdn.jsdelivr.net/npm/@ruby/head-wasm-wasi@latest/dist/", | ||
); | ||
await page.setContent(` | ||
<script src="browser.script.iife.js"></script> | ||
<script type="text/ruby" data-eval="async"> | ||
require 'js/require_remote' | ||
JS::RequireRemote.instance.load 'fixtures/error_on_load_twice' | ||
JS.global.checkResolved JS::RequireRemote.instance.load 'fixtures/error_on_load_twice' | ||
</script> | ||
`); | ||
|
||
expect(await resolve()).toBe(false); | ||
}); | ||
|
||
test("JS::RequireRemote#load throws error when gem is not found", async ({ | ||
page, | ||
}) => { | ||
// Opens the URL that will be used as the basis for determining the relative URL. | ||
await page.goto( | ||
"https://cdn.jsdelivr.net/npm/@ruby/head-wasm-wasi@latest/dist/", | ||
); | ||
await page.setContent(` | ||
<script src="https://cdn.jsdelivr.net/npm/@ruby/head-wasm-wasi@latest/dist/browser.script.iife.js"> | ||
</script> | ||
<script type="text/ruby" data-eval="async"> | ||
require 'js/require_remote' | ||
JS::RequireRemote.instance.load 'foo' | ||
</script> | ||
`); | ||
|
||
const error = await page.waitForEvent("pageerror"); | ||
expect(error.message).toMatch(/cannot load such url -- .+\/foo.rb/); | ||
}); | ||
|
||
// TODO: This test fails. | ||
// In the integration test, response#url returns the URL before the redirect. | ||
// I do not know the cause. Under investigation. | ||
test.skip("JS::RequireRemote#load identifies by URL after redirect", async ({ | ||
page, | ||
context, | ||
}) => { | ||
// Stop tests immediately when an error occurs in the page. | ||
page.on("pageerror", (error) => { | ||
throw error; | ||
}); | ||
|
||
// Use the proxy to redirect the request. | ||
context.route(/redirect/, (route) => { | ||
route.fulfill({ | ||
status: 302, | ||
headers: { | ||
location: "error_on_load_twice.rb", | ||
}, | ||
}); | ||
}); | ||
|
||
const resolve = await resolveBinding(page, "checkResolved"); | ||
await page.goto( | ||
"https://cdn.jsdelivr.net/npm/@ruby/head-wasm-wasi@latest/dist/", | ||
); | ||
await page.setContent(` | ||
<script src="browser.script.iife.js"></script> | ||
<script type="text/ruby" data-eval="async"> | ||
require 'js/require_remote' | ||
JS::RequireRemote.instance.load 'redirect_to_error_on_load_twice' | ||
JS.global.checkResolved JS::RequireRemote.instance.load 'error_on_load_twice' | ||
</script> | ||
`); | ||
|
||
expect(await resolve()).toBe(false); | ||
}); | ||
}); | ||
} |
Oops, something went wrong.