Skip to content
This repository was archived by the owner on Nov 20, 2023. It is now read-only.

Commit 4eaf5c3

Browse files
committed
Initial commit
0 parents  commit 4eaf5c3

File tree

10 files changed

+656
-0
lines changed

10 files changed

+656
-0
lines changed

.gitignore

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/dist
2+
/head-wasm32-unknown-wasi-full-js/
3+
/lib
4+
/node_modules/
5+
/ruby.wasm
6+
/src/app.wasm
7+
/wasi-vfs
8+
/yarn-error.log

Gemfile

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# frozen_string_literal: true
2+
3+
source "https://rubygems.org"
4+
5+
gem "bundler"
6+
gem "rake"
7+
8+
gem "syntax_tree"
9+
gem "syntax_tree-haml"
10+
gem "syntax_tree-rbs"

Gemfile.lock

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
GEM
2+
remote: https://rubygems.org/
3+
specs:
4+
haml (5.2.2)
5+
temple (>= 0.8.0)
6+
tilt
7+
prettier_print (0.1.0)
8+
rake (13.0.6)
9+
rbs (2.6.0)
10+
syntax_tree (3.3.0)
11+
prettier_print
12+
syntax_tree-haml (1.3.1)
13+
haml (>= 5.2)
14+
prettier_print
15+
syntax_tree (>= 2.0.1)
16+
syntax_tree-rbs (0.5.0)
17+
prettier_print
18+
rbs
19+
syntax_tree (>= 2.0.1)
20+
temple (0.8.2)
21+
tilt (2.0.11)
22+
23+
PLATFORMS
24+
x86_64-darwin-21
25+
26+
DEPENDENCIES
27+
bundler
28+
rake
29+
syntax_tree
30+
syntax_tree-haml
31+
syntax_tree-rbs
32+
33+
BUNDLED WITH
34+
2.3.14

Rakefile

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# frozen_string_literal: true
2+
3+
require "rake/clean"
4+
5+
desc "Download and unzip the wasi-vfs executable"
6+
file "wasi-vfs" do
7+
version = "0.1.1"
8+
filename =
9+
if ENV["CI"]
10+
"wasi-vfs-cli-x86_64-unknown-linux-gnu.zip"
11+
else
12+
"wasi-vfs-cli-x86_64-apple-darwin.zip"
13+
end
14+
15+
`curl -LO "https://github.com/kateinoigakukun/wasi-vfs/releases/download/v#{version}/#{filename}"`
16+
`unzip #{filename}`
17+
rm filename
18+
end
19+
20+
desc "Download and untar the Ruby WASI directory"
21+
directory "head-wasm32-unknown-wasi-full-js" do
22+
require "json"
23+
version = JSON.parse(File.read("package.json"))["dependencies"]["ruby-head-wasm-wasi"][1..]
24+
filename = "ruby-head-wasm32-unknown-wasi-full-js.tar.gz"
25+
26+
`curl -LO https://github.com/ruby/ruby.wasm/releases/download/ruby-head-wasm-wasi-#{version}/#{filename}`
27+
`tar xfz #{filename}`
28+
rm filename
29+
end
30+
31+
desc "Extract the Ruby executable from the WASI directory"
32+
file "ruby.wasm" => ["head-wasm32-unknown-wasi-full-js"] do
33+
cp "head-wasm32-unknown-wasi-full-js/usr/local/bin/ruby", "ruby.wasm"
34+
end
35+
36+
desc "Build the app.wasm file with additional files built in"
37+
file "src/app.wasm" => ["Gemfile.lock", "wasi-vfs", "ruby.wasm"] do
38+
require "bundler/setup"
39+
40+
loaded_before = $LOADED_FEATURES.dup
41+
require "syntax_tree"
42+
require "syntax_tree/haml"
43+
# require "syntax_tree/rbs"
44+
loaded_after = $LOADED_FEATURES - loaded_before
45+
46+
filepaths = loaded_after.group_by { |filepath| filepath[%r{/gems/(.+?)-\d}, 1] }
47+
gem_names = filepaths.keys.tap { |names| names.delete(nil) }.sort
48+
puts "Gems loaded: #{gem_names.join(", ")}"
49+
50+
mkdir "lib"
51+
gem_names.each do |gem_name|
52+
libdir = $:.find { _1.match?(%r{/#{gem_name}-\d}) }
53+
54+
Dir["#{libdir}/**/*"].sort.each do |filepath|
55+
relative = "lib#{filepath.delete_prefix(libdir)}"
56+
57+
if File.directory?(filepath)
58+
mkdir_p relative
59+
else
60+
cp filepath, relative
61+
end
62+
end
63+
end
64+
65+
`./wasi-vfs pack ruby.wasm --mapdir /lib::./lib --mapdir /usr::./head-wasm32-unknown-wasi-full-js/usr -o src/app.wasm`
66+
rm_rf "lib"
67+
end
68+
69+
CLOBBER.concat(%w[wasi-vfs head-wasm32-unknown-wasi-full-js ruby.wasm src/app.wasm lib])
70+
71+
task default: "src/app.wasm"

bin/build

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
#!/usr/bin/env node
2+
3+
const esbuild = require("esbuild");
4+
const path = require("path");
5+
6+
const wasmPlugin = {
7+
name: "wasm",
8+
setup(build) {
9+
let path = require("path")
10+
let fs = require("fs")
11+
12+
// Resolve ".wasm" files to a path with a namespace
13+
build.onResolve({ filter: /\.wasm$/ }, (args) => {
14+
// If this is the import inside the stub module, import the
15+
// binary itself. Put the path in the "wasm-binary" namespace
16+
// to tell our binary load callback to load the binary file.
17+
if (args.namespace === "wasm-stub") {
18+
return { path: args.path, namespace: "wasm-binary" };
19+
}
20+
21+
// Otherwise, generate the JavaScript stub module for this
22+
// ".wasm" file. Put it in the "wasm-stub" namespace to tell
23+
// our stub load callback to fill it with JavaScript.
24+
//
25+
// Resolve relative paths to absolute paths here since this
26+
// resolve callback is given "resolveDir", the directory to
27+
// resolve imports against.
28+
if (args.resolveDir === "") {
29+
return; // Ignore unresolvable paths
30+
}
31+
32+
return {
33+
path: path.isAbsolute(args.path) ? args.path : path.join(args.resolveDir, args.path),
34+
namespace: "wasm-stub"
35+
};
36+
});
37+
38+
// Virtual modules in the "wasm-stub" namespace are filled with
39+
// the JavaScript code for compiling the WebAssembly binary. The
40+
// binary itself is imported from a second virtual module.
41+
build.onLoad({ filter: /.*/, namespace: "wasm-stub" }, async (args) => ({
42+
contents: `import wasm from ${JSON.stringify(args.path)};
43+
export default (imports) => WebAssembly.instantiate(wasm, imports).then((result) => result.instance);`,
44+
}));
45+
46+
// Virtual modules in the "wasm-binary" namespace contain the
47+
// actual bytes of the WebAssembly file. This uses esbuild's
48+
// built-in "binary" loader instead of manually embedding the
49+
// binary data inside JavaScript code ourselves.
50+
build.onLoad({ filter: /.*/, namespace: "wasm-binary" }, async (args) => ({
51+
contents: await fs.promises.readFile(args.path),
52+
loader: "binary",
53+
}));
54+
}
55+
};
56+
57+
esbuild.build({
58+
bundle: true,
59+
entryPoints: [path.join(__dirname, "../src/index.ts")],
60+
format: "cjs",
61+
minify: true,
62+
outdir: path.join(__dirname, "../dist"),
63+
platform: "node",
64+
plugins: [wasmPlugin],
65+
sourcemap: false,
66+
target: "es6"
67+
});

package.json

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"name": "node-syntax-tree",
3+
"version": "0.1.0",
4+
"description": "A node package for accessing Syntax Tree through WASM",
5+
"main": "dist/index.js",
6+
"scripts": {},
7+
"repository": {
8+
"type": "git",
9+
"url": "git+https://github.com/ruby-syntax-tree/node-syntax-tree.git"
10+
},
11+
"author": "Kevin Newton",
12+
"license": "MIT",
13+
"bugs": {
14+
"url": "https://github.com/ruby-syntax-tree/node-syntax-tree/issues"
15+
},
16+
"homepage": "https://github.com/ruby-syntax-tree/node-syntax-tree#readme",
17+
"dependencies": {
18+
"@wasmer/wasi": "^0.12.0",
19+
"@wasmer/wasmfs": "^0.12.0",
20+
"path-browserify": "^1.0.1",
21+
"ruby-head-wasm-wasi": "^0.3.0"
22+
},
23+
"devDependencies": {
24+
"@types/node": "^17.0.33",
25+
"@types/path-browserify": "^1.0.0",
26+
"esbuild": "^0.14.39"
27+
}
28+
}

src/app.d.ts

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
declare module "*.wasm" {
2+
type Exports = {
3+
_initialize: () => void;
4+
memory: WebAssembly.Memory;
5+
};
6+
7+
type Instance = WebAssembly.Instance & { exports: Exports };
8+
export default function load(imports: any): Promise<Instance>;
9+
}

src/index.ts

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import crypto from "crypto";
2+
import { WASI } from "@wasmer/wasi";
3+
import { WasmFs } from "@wasmer/wasmfs";
4+
import path from "path-browserify";
5+
import { RubyVM } from "ruby-head-wasm-wasi/dist/index";
6+
7+
import load from "./app.wasm";
8+
9+
// This overwrites the default writeSync function used by the WasmFs to instead
10+
// pipe it out to the console.
11+
function createWriter(originalWriter: Function) {
12+
return function () {
13+
let text: string;
14+
15+
if (arguments.length === 4) {
16+
text = arguments[1];
17+
} else {
18+
text = new TextDecoder("utf-8").decode(arguments[1]);
19+
}
20+
21+
switch (arguments[0]) {
22+
case 1:
23+
console.log(text);
24+
break;
25+
case 2:
26+
console.warn(text);
27+
break;
28+
}
29+
30+
return originalWriter.call(arguments);
31+
}
32+
}
33+
34+
export default async function createRuby() {
35+
// First, create a new file system that we can use internally within the Ruby
36+
// WASM VM.
37+
const wasmFs = new WasmFs();
38+
wasmFs.fs.mkdirSync("/tmp", 0o777);
39+
wasmFs.fs.writeSync = createWriter(wasmFs.fs.writeSync.bind(wasmFs.fs));
40+
41+
// Next, create a new WASI instance with the correct options overridden from
42+
// the defaults.
43+
const wasi = new WASI({
44+
bindings: {
45+
...WASI.defaultBindings,
46+
fs: wasmFs.fs,
47+
path: path,
48+
randomFillSync: crypto.randomFillSync as typeof WASI.defaultBindings.randomFillSync,
49+
},
50+
preopens: { "/": "/tmp" }
51+
});
52+
53+
// Then, create a new Ruby VM instance that we can use to store the memory for
54+
// our application.
55+
const rubyVM = new RubyVM();
56+
const imports = { wasi_snapshot_preview1: wasi.wasiImport };
57+
rubyVM.addToImports(imports);
58+
59+
// Set the WASI memory to use the memory for our application.
60+
const instance = await load(imports);
61+
wasi.setMemory(instance.exports.memory);
62+
63+
// Load our application into the virtual machine.
64+
instance.exports._initialize();
65+
await rubyVM.setInstance(instance);
66+
67+
// Initial our virtual machine and return it. It should now be able to
68+
// evaluate and execute Ruby code.
69+
rubyVM.initialize();
70+
71+
// Once our virtual machine is booted, we're going to require the necessary
72+
// files to make it work. I'm not sure why I need to explicitly require
73+
// did_you_mean here, but it doesn't work without it.
74+
rubyVM.eval(`
75+
require "did_you_mean"
76+
require "json"
77+
78+
$:.unshift("/lib")
79+
require_relative "/lib/syntax_tree"
80+
require_relative "/lib/syntax_tree/haml"
81+
# require_relative "/lib/syntax_tree/rbs"
82+
`);
83+
84+
return {
85+
rubyVM,
86+
formatHAML(source: string) {
87+
return format(source, ".haml");
88+
},
89+
formatRuby(source: string) {
90+
return format(source, ".rb");
91+
},
92+
// formatRBS(source: string) {
93+
// return format(source, ".rbs");
94+
// }
95+
};
96+
97+
function format(source: string, kind: string) {
98+
const jsonSource = JSON.stringify(JSON.stringify(source));
99+
const rubySource = `SyntaxTree::HANDLERS.fetch("${kind}").format(JSON.parse(${jsonSource}))`;
100+
return rubyVM.eval(rubySource).toString();
101+
}
102+
};
103+
104+
export type Ruby = Awaited<ReturnType<typeof createRuby>>;

tsconfig.json

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"compilerOptions": {
3+
"esModuleInterop": true,
4+
"module": "commonjs",
5+
"moduleResolution": "node",
6+
"noImplicitAny": true,
7+
"strict": true
8+
},
9+
"include": ["src"]
10+
}

0 commit comments

Comments
 (0)