Skip to content

Commit

Permalink
Refine gem installation
Browse files Browse the repository at this point in the history
  • Loading branch information
skryukov committed Jan 29, 2024
1 parent 571d8dc commit 086d1de
Show file tree
Hide file tree
Showing 9 changed files with 864 additions and 32 deletions.
784 changes: 783 additions & 1 deletion package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
"@vitejs/plugin-react": "^4.2.1",
"prettier": "^3.2.4",
"typescript": "^5.3.3",
"vite": "^5.0.12"
"vite": "^5.0.12",
"vite-plugin-svgr": "^4.2.0"
},
"optionalDependencies": {
"@rollup/rollup-linux-x64-gnu": "4.9.6"
Expand Down
56 changes: 43 additions & 13 deletions src/components/App/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@ import Editor from "@monaco-editor/react";

import { runWASI } from "../../engines/wasi";
import cs from "./styles.module.css";
import { RbValue } from "@ruby/wasm-wasi";
import SvgSpinner from "./spinner.svg?react";

const initialRubyCode = `require "uri-idna"
const initialRubyCode = `# This is a Ruby WASI playground
# You can run any Ruby code here and see the result
# You can also install gems and use them in your code (unless they use native extensions)
# For example, try installing the URI::IDNA gem and using it in your code:
require "uri-idna"
URI::IDNA.register(alabel: "xn--gdkl8fhk5egc.jp", ulabel: "ハロー・ワールド.jp")
`;
Expand All @@ -16,9 +22,10 @@ export default function App() {
const [result, setResult] = useState("Press run...");
const [log, setLog] = useState<string[]>([]);
const [editorValueSource, setEditorValueSource] = useState<"result" | "logs">("result");
const [installedGems, setInstalledGems] = useState<string[]>([]);
// object of gems and their versions as values
const [installedGems, setInstalledGems] = useState<{ [key: string]: string|null }>({});

const runVM = (executeCode?: string, onSuccess?: Function, onError?: Function ) => {
const runVM = (executeCode?: string, onSuccess?: (result: RbValue) => void, onError?: Function) => {
setLoading(true);
setLog([]);
setResult("");
Expand All @@ -31,25 +38,33 @@ export default function App() {
setLog((old) => [...old, `[error] ${line}`]);
};
runWASI({ code: (executeCode || code), setResult, setStdout, setStderr })
.then((result) => {
setEditorValueSource("result");
onSuccess && onSuccess(result);
})
.catch((err) => {
setLog((old) => [...old, `[error] ${err}`]);
setEditorValueSource("logs");
onError && onError(err);
})
.then(() => {
setEditorValueSource("result");
onSuccess && onSuccess();
})
.finally(() => setLoading(false));
};

const installGem = () => {
if (!gem) {
return;
}
setInstalledGems((old) => ({ ...old, [gem]: null }));
runVM(
`Gem::Commands::InstallCommand.new.install_gem("${gem}", nil)`,
() => setInstalledGems((old) => [...old, gem]),
`a = Gem::Commands::InstallCommand.new; a.install_gem("${gem}", nil); a.installed_specs.map { [_1.name, _1.version.to_s] }.to_h`,
(result) => {
const version = result.toJS()[gem];
setInstalledGems((old) => ({ ...old, [gem]: version }));
},
() => setInstalledGems((old) => {
const { [gem]: _, ...rest } = old;
return rest;
})
);
setGem("");
};
Expand Down Expand Up @@ -81,9 +96,11 @@ export default function App() {
</button>
</div>
<div className={cs.menuDependencies}>
{installedGems.map((gem) => (
{Object.keys(installedGems).map((gem) => (
<div className={cs.menuDependency} key={gem}>
{gem}
{gem} {installedGems[gem] ? `(${installedGems[gem]})` : (
<SvgSpinner className={cs.menuSpinner}/>
)}
</div>
))}
</div>
Expand All @@ -99,7 +116,12 @@ export default function App() {
defaultValue={code}
onChange={handleEditorChange}
onMount={() => setLoading(false)}
options={{ wordWrap: "on", minimap: { enabled: false }, overviewRulerBorder: false }}
options={{
wordWrap: "on",
minimap: { enabled: false },
overviewRulerBorder: false,
hideCursorInOverviewRuler: true
}}
/>
</div>
<div className={cs.editorFooter}>
Expand Down Expand Up @@ -130,7 +152,15 @@ export default function App() {
theme="vs-dark"
value={editorValueSource === "result" ? result : log.join("\n")}
language="shell"
options={{wordWrap: "on", lineNumbers: "off", readOnly: true, minimap: { enabled: false }, overviewRulerBorder: false, renderLineHighlight: "none" }}
options={{
wordWrap: "on",
lineNumbers: "off",
readOnly: true,
minimap: { enabled: false },
overviewRulerBorder: false,
renderLineHighlight: "none",
hideCursorInOverviewRuler: true
}}
/>
</div>
</div>
Expand Down
1 change: 1 addition & 0 deletions src/components/App/spinner.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions src/components/App/styles.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,15 @@
padding-bottom: 4px;
}

.menuSpinner {
fill: #f1f1f1;
width: 16px;
height: 16px;
margin-left: 8px;
display: inline-block;
vertical-align: middle;
}

.editor {
grid-area: editor;
display: flex;
Expand Down
28 changes: 15 additions & 13 deletions src/engines/wasi/wasi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { RubyVM } from "@ruby/wasm-wasi";
import { Fd, PreopenDirectory, File, WASI, OpenFile, ConsoleStdout, Directory } from "@bjorn3/browser_wasi_shim";

import wasmUrl from "@ruby/3.3-wasm-wasi/dist/ruby+stdlib.wasm?url";

const wasmModulePromise = WebAssembly.compileStreaming(await fetch(wasmUrl));

import { TRunParams, TSetString } from "../types";

import socketrb from "./../../stubs/socket.rb?url";
Expand All @@ -16,13 +19,15 @@ export const wasmPathToLib = "/usr/local/lib/ruby_gems";
export const gemdir = new PreopenDirectory(wasmPathToLib, {})

async function createRuby(setStdout: TSetString, setStderr: TSetString) {
gemdir.dir.contents = {
"socket.rb": new File(await (await fetch(socketrb)).arrayBuffer()),
if (Object.values(gemdir.dir.contents).length === 0) {
gemdir.dir.contents = {
"socket.rb": new File(await (await fetch(socketrb)).arrayBuffer()),
"thread_stub.rb": new File(await (await fetch(threadrb)).arrayBuffer()),
"rubygems_stub.rb": new File(await (await fetch(rubygemsrb)).arrayBuffer()),
"io": new Directory({
"wait.rb": new File(await (await fetch(iowaitrb)).arrayBuffer()),
})
"wait.rb": new File(await (await fetch(iowaitrb)).arrayBuffer()),
})
}
}

const fds: Fd[] = [
Expand All @@ -42,7 +47,7 @@ async function createRuby(setStdout: TSetString, setStderr: TSetString) {
};
ruby.addToImports(imports);

const { instance } = await WebAssembly.instantiateStreaming(await fetch(wasmUrl), imports);
const instance = await WebAssembly.instantiate(await wasmModulePromise, imports);
await ruby.setInstance(instance);

wasi.initialize(instance as any);
Expand All @@ -51,14 +56,11 @@ async function createRuby(setStdout: TSetString, setStderr: TSetString) {
return ruby;
}

let vm: RubyVM;

export const run = async (params: TRunParams) => {
const { code, setResult, setStdout, setStderr } = params;
if (vm === undefined) {
vm = await createRuby(setStdout, setStderr);
}

const result = await vm.evalAsync(code)
setResult(result.toString());
const vm = await createRuby(setStdout, setStderr);
const jsResult = await vm.evalAsync(code)
const result = jsResult.toString()
setResult(result);
return jsResult;
};
7 changes: 6 additions & 1 deletion src/stubs/rubygems_stub.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@

Gem::Request.prepend(Module.new do
def perform_request(request)
js_response = JS.global.fetch(Gem::Uri.redact(@uri).to_s, {method: request.method}).await
js_response = JS.global.fetch(
"https://corsproxy.io/?#{JS.global.encodeURIComponent(Gem::Uri.redact(@uri).to_s)}",
{
method: request.method,
},
).await
code = js_response[:status]
array_buffer = js_response.arrayBuffer.await
base64_body = JS.global.btoa(JS.global[:String][:fromCharCode].apply(nil, JS.global[:Uint8Array].new(array_buffer))).to_s
Expand Down
1 change: 1 addition & 0 deletions src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
/// <reference types="vite/client" />
/// <reference types="vite-plugin-svgr/client" />
7 changes: 4 additions & 3 deletions vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { defineConfig } from "vite"
import react from "@vitejs/plugin-react"
import svgr from "vite-plugin-svgr";

// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
plugins: [react(), svgr()],
base: "/ruby-wasi-playground/",
server: {
headers: {
Expand Down

0 comments on commit 086d1de

Please sign in to comment.