Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/cheat_sheet.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ The easiest way to run Ruby on browser is to use `browser.script.iife.js` script
</html>
```

Use `data-env` on the `browser.script.iife.js` script tag to pass environment variables when the Ruby VM starts:

```html
<script
src="https://cdn.jsdelivr.net/npm/@ruby/4.0-wasm-wasi@2.9.4/dist/browser.script.iife.js"
data-env="RUBY_BOX=1 RUBY_FIBER_MACHINE_STACK_SIZE=1048576"
></script>
```

If you want to control Ruby VM from JavaScript, you can use `@ruby/wasm-wasi` package API:

```html
Expand Down
1 change: 1 addition & 0 deletions packages/npm-packages/ruby-wasm-wasi/example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ This is a simple example of how to use the `ruby-wasm-wasi` family packages
$ ruby -run -e httpd . -p 8000
$ # Open http://localhost:8000/hello.html
$ # Open http://localhost:8000/lucky.html
$ # Open http://localhost:8000/ruby-box.html
$ # Open http://localhost:8000/script-src
```

Expand Down
22 changes: 22 additions & 0 deletions packages/npm-packages/ruby-wasm-wasi/example/ruby-box.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<html>
<script
src="https://cdn.jsdelivr.net/npm/@ruby/head-wasm-wasi@2.9.4/dist/browser.script.iife.js"
data-env="RUBY_BOX=1"
></script>
<div id="result">
<div id="enabled"></div>
<div id="constant"></div>
</div>
<script type="text/ruby">
require "js"

box = Ruby::Box.new
box.eval <<~RUBY
X = 123
RUBY

document = JS.global[:document]
document.getElementById("enabled")[:innerText] = "Ruby::Box.enabled?: #{Ruby::Box.enabled?}"
document.getElementById("constant")[:innerText] = "box::X: #{box::X}"
</script>
</html>
43 changes: 42 additions & 1 deletion packages/npm-packages/ruby-wasm-wasi/src/browser.script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,18 @@ export const main = async (
pkg: { name: string; version: string },
options?: Parameters<typeof DefaultRubyVM>[1],
) => {
const scriptEnv = deriveEnv(document.currentScript);
const response = fetch(
`https://cdn.jsdelivr.net/npm/${pkg.name}@${pkg.version}/dist/ruby+stdlib.wasm`,
);
const module = await compileWebAssemblyModule(response);
const { vm } = await DefaultRubyVM(module, options);
const { vm } = await DefaultRubyVM(module, {
...options,
env: {
...scriptEnv,
...options?.env,
},
});
await mainWithRubyVM(vm);
};

Expand All @@ -24,12 +31,18 @@ export const componentMain = async (
options: {
instantiate: RubyComponentInstantiator;
wasip2: any;
env?: Record<string, string> | undefined;
}
) => {
const scriptEnv = deriveEnv(document.currentScript);
const componentUrl = `https://cdn.jsdelivr.net/npm/${pkg.name}@${pkg.version}/dist/component`;
const fetchComponentFile = (relativePath: string) => fetch(`${componentUrl}/${relativePath}`);
const { vm } = await RubyVM.instantiateComponent({
...options,
env: {
...scriptEnv,
...options.env,
},
getCoreModule: (relativePath: string) => {
const response = fetchComponentFile(relativePath);
return compileWebAssemblyModule(response);
Expand Down Expand Up @@ -90,6 +103,34 @@ const deriveEvalStyle = (tag: Element): "async" | "sync" => {
return rawEvalStyle;
};

const deriveEnv = (tag: Element | null): Record<string, string> => {
const rawEnv = tag?.getAttribute("data-env");
if (!rawEnv) {
return {};
}

const trimmedEnv = rawEnv.trim();
if (!trimmedEnv) {
return {};
}

return trimmedEnv
.split(/\s+/)
.reduce<Record<string, string>>((env, entry) => {
const delimiterIndex = entry.indexOf("=");
if (delimiterIndex <= 0) {
console.warn(
`data-env entry must be in the KEY=value format. ${entry} is ignored.`,
);
return env;
}

// Only the first "=" separates key and value so values can contain "=".
env[entry.slice(0, delimiterIndex)] = entry.slice(delimiterIndex + 1);
return env;
}, {});
};

const loadScriptAsync = async (
tag: Element,
): Promise<{ scriptContent: string; evalStyle: "async" | "sync" } | null> => {
Expand Down
16 changes: 15 additions & 1 deletion packages/npm-packages/ruby-wasm-wasi/src/vm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ export type RubyInitComponentOptions = {
*/
wasip2: any;

/**
* Environment variables to pass to the Ruby VM.
*/
env?: Record<string, string> | undefined;

/**
* The arguments to pass to the Ruby VM. Note that the first argument must be the Ruby program name.
*
Expand Down Expand Up @@ -179,9 +184,18 @@ export class RubyVM {
initComponent = async (jsRuntime) => {
const { instantiate, getCoreModule, wasip2 } = options;
const { cli, clocks, filesystem, io, random, sockets, http } = wasip2;
const environment = options.env ? {
...cli.environment,
getEnvironment: () => Array.from(
new Map([
...cli.environment.getEnvironment(),
...Object.entries(options.env ?? {}),
]).entries(),
),
} : cli.environment;
const importObject = {
"ruby:js/js-runtime": jsRuntime,
"wasi:cli/environment": cli.environment,
"wasi:cli/environment": environment,
"wasi:cli/exit": cli.exit,
"wasi:cli/stderr": cli.stderr,
"wasi:cli/stdin": cli.stdin,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,22 @@ import { readFileSync } from "fs";
import http from "http";
import https from "https";

const ruby4OrLater = () => {
const packageName = path.basename(
process.env.RUBY_NPM_PACKAGE_ROOT ?? "ruby-head-wasm-wasi",
);
if (packageName.startsWith("ruby-head-")) {
return true;
}

const match = packageName.match(/^ruby-(\d+)\.(\d+)-wasm-wasi/);
if (!match) {
return false;
}

return Number(match[1]) >= 4;
};

test.beforeEach(async ({ context, page }) => {
setupDebugLog(context);
setupUncaughtExceptionRejection(page);
Expand Down Expand Up @@ -55,6 +71,19 @@ test("lucky.html is healthy", async ({ page }) => {
expect(result).toMatch(/(Lucky|Unlucky)/);
});

test.describe("ruby-box.html", () => {
test.skip(!ruby4OrLater(), "Ruby::Box is available on Ruby 4.0 or later");

test("is healthy", async ({ page }) => {
await page.goto("/ruby-box.html");
await waitForRubyVM(page);
await expect(page.locator("#enabled")).toHaveText(
"Ruby::Box.enabled?: true",
);
await expect(page.locator("#constant")).toHaveText("box::X: 123");
});
});

test("script-src/index.html is healthy", async ({ page }) => {
const messages: string[] = [];
page.on("console", (msg) => messages.push(msg.text()));
Expand All @@ -80,7 +109,7 @@ if (process.env.RUBY_NPM_PACKAGE_ROOT) {
await page.goto("/require_relative/index.html");

await waitForRubyVM(page);
while (!messages.some((msg) => /Hello, world\!/.test(msg))) {
while (!messages.some((msg) => /Hello, world\!/.test(msg))) {
await page.waitForEvent("console");
}
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { test, expect } from "@playwright/test";

import {
setupDebugLog,
setupProxy,
setupUncaughtExceptionRejection,
resolveBinding,
} from "../support";

if (!process.env.RUBY_NPM_PACKAGE_ROOT) {
test.skip("skip", () => {});
} else {
test.beforeEach(async ({ context, page }) => {
setupDebugLog(context);
setupProxy(context);
setupUncaughtExceptionRejection(page);
});

test.describe("data-env", () => {
test("passes environment variables to the Ruby VM", async ({ page }) => {
const resolve = await resolveBinding(page, "checkResolved");
await page.setContent(`
<script
src="https://cdn.jsdelivr.net/npm/@ruby/head-wasm-wasi@latest/dist/browser.script.iife.js"
data-env="RUBY_WASM_TEST=ok RUBY_WASM_TEST_EQUALS=a=b"
></script>
<script type="text/ruby" data-eval="async">
require "js"
JS.global.checkResolved [ENV["RUBY_WASM_TEST"], ENV["RUBY_WASM_TEST_EQUALS"]].join(",")
</script>
`);
expect(await resolve()).toBe("ok,a=b");
});
});
}
Loading