webvas 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -5
- data/LICENSE.txt +1 -1
- data/README.md +68 -77
- data/exe/webvas +3 -267
- data/js/bridge.js +117 -19
- data/js/loader.js +114 -52
- data/{site → js}/worker.js +2 -30
- data/lib/webvas/backend.rb +8 -1
- data/lib/webvas/bridge.rb +4 -5
- data/lib/webvas/cli.rb +319 -0
- data/lib/webvas/shader.rb +4 -27
- data/lib/webvas/version.rb +1 -1
- data/lib/webvas.rb +4 -3
- data/sig/webvas.rbs +8 -11
- metadata +5 -35
- data/docs/performance.md +0 -12
- data/docs/security.md +0 -9
- data/docs/spikes.md +0 -18
- data/runtime/Gemfile +0 -22
- data/runtime/Gemfile.lock +0 -101
- data/script/build_site +0 -52
- data/site/app.js +0 -466
- data/site/index.html +0 -56
- data/site/loader-example.html +0 -26
- data/site/style.css +0 -89
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8a2023d01aad96f20ed9938d18ddedf254edd56ba69c2dc0383a84d00c2efd03
|
|
4
|
+
data.tar.gz: 4ad4fbd5919583226a281186a717d59eb038a0d1aac25e79716ccd8cc5775fb9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 034c1b98ebf7a2607d8b985c21e63394ccca5b228bf99fc39dc74bb710ea6ae4e72b2ae13968e7b8108f3f3a71280c0119ab025290274d1b8b877b3bba3aeb78
|
|
7
|
+
data.tar.gz: affd813bbbbf4ab437bfa794c5d56dd54187c27e4c6315bec9ad76b40bde5f9fae4dfa6941641e668f56f8a32506c40efe9c0e7983a5b5da32206a03209f1439
|
data/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.1] - 2026-09-25
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Build the current WebAssembly runtime before browser tests and Pages deployment.
|
|
8
|
+
- Include the CLI executable and runtime build inputs in the gem.
|
|
9
|
+
- Require a Gesso release that supports browser sketches.
|
|
10
|
+
|
|
3
11
|
## [0.3.0] - 2026-09-25
|
|
4
12
|
|
|
5
13
|
### Added
|
|
6
14
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
|
|
15
|
+
- Gesso sketches and RLSL WGSL shaders in the browser runtime.
|
|
16
|
+
- An editable playground with example modes, compressed share URLs, and standalone HTML downloads.
|
|
17
|
+
- `webvas new`, `serve`, and `build` commands, including custom WebAssembly runtime builds.
|
|
18
|
+
- Worker reset between runs, Ruby error reporting, and frame performance measurements.
|
|
19
|
+
|
|
20
|
+
## [0.1.0] - 2026-09-25
|
|
21
|
+
|
|
22
|
+
Initial release
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
|
@@ -2,130 +2,121 @@
|
|
|
2
2
|
|
|
3
3
|
# Webvas
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**Write Ruby graphics sketches and run them in a browser.**
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
[Play with Webvas](https://rbgfx.github.io/webvas/) · [Ruby API](#ruby-api) · [Build the playground](#build-the-playground)
|
|
7
|
+
[Playground](https://rbgfx.github.io/webvas/) · [Ruby API](#ruby-api) · [CLI](#cli) · [Security](#security)
|
|
10
8
|
|
|
11
9
|
</div>
|
|
12
10
|
|
|
13
|
-
|
|
11
|
+
Webvas connects RBGL's software renderer to an `OffscreenCanvas` and advances one frame at a time with `requestAnimationFrame`. The playground includes RBGL, Gesso, and RLSL examples, shareable source links, and single-file HTML downloads.
|
|
14
12
|
|
|
15
|
-
|
|
13
|
+
## Features
|
|
16
14
|
|
|
17
|
-
|
|
15
|
+
- RBGL canvas backend with pointer, keyboard, and wheel input
|
|
16
|
+
- Gesso sketch runner and RLSL WGSL compute shaders
|
|
17
|
+
- Fresh Ruby worker for each playground run, with visible errors and frame measurements
|
|
18
|
+
- Deflate-compressed source in URL fragments; source is not sent to a Webvas service
|
|
19
|
+
- `webvas new`, `serve`, and `build` for standalone sketch projects
|
|
20
|
+
- Browser runtime built with Webvas, RBGL, Tessel, Gesso, Glyphic, and RLSL
|
|
18
21
|
|
|
19
|
-
##
|
|
22
|
+
## Quick start
|
|
20
23
|
|
|
21
|
-
|
|
24
|
+
Add the loader and a Ruby source file to an HTML page:
|
|
22
25
|
|
|
23
|
-
~~~
|
|
24
|
-
|
|
26
|
+
~~~html
|
|
27
|
+
<canvas id="screen" width="320" height="240"></canvas>
|
|
28
|
+
<script type="text/ruby" data-webvas data-canvas="#screen" src="app.rb"></script>
|
|
29
|
+
<script src="https://rbgfx.github.io/webvas/loader.js"
|
|
30
|
+
data-runtime="https://rbgfx.github.io/webvas/assets/webvas.wasm"></script>
|
|
25
31
|
~~~
|
|
26
32
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
## Ruby API
|
|
33
|
+
In `app.rb`:
|
|
30
34
|
|
|
31
35
|
~~~ruby
|
|
32
|
-
require "
|
|
33
|
-
require "rbgl"
|
|
34
|
-
|
|
35
|
-
backend = Webvas::Backend.new(width: 480, height: 320)
|
|
36
|
-
window = RBGL::GUI::Window.new(width: 480, height: 320, backend: backend)
|
|
36
|
+
require "gesso"
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
Gesso.run(width: 320, height: 240, runner: :web) do
|
|
39
|
+
draw do
|
|
40
|
+
background "#101827"
|
|
41
|
+
no_stroke
|
|
42
|
+
fill "#f07850"
|
|
43
|
+
circle width / 2, height / 2, 36
|
|
44
|
+
end
|
|
41
45
|
end
|
|
42
46
|
~~~
|
|
43
47
|
|
|
44
|
-
|
|
48
|
+
The official runtime is built with these gems already included. A runtime URL can be set with `data-runtime`; use the same URL for sketches that bundle their own dependencies.
|
|
45
49
|
|
|
46
|
-
|
|
50
|
+
## Ruby API
|
|
51
|
+
|
|
52
|
+
Create an `RBGL::GUI::Window` with `Webvas::Backend.new`, then call `Webvas.run(window)` with a frame block. The block receives the RBGL context and delta time. `Webvas.run` reuses one animation callback, closes the window on stop or error, and reports exceptions through the bridge.
|
|
53
|
+
|
|
54
|
+
`Webvas::Backend` accepts `width`, `height`, `canvas`, `title`, and `pixelated`. Canvas drawing dimensions set render resolution; CSS scales its display. Pointer coordinates are mapped from CSS pixels to render pixels. Focus the canvas to send keyboard input.
|
|
55
|
+
|
|
56
|
+
RLSL can generate the WGSL source for WebGPU:
|
|
47
57
|
|
|
48
58
|
~~~ruby
|
|
49
|
-
require "
|
|
59
|
+
require "rlsl"
|
|
60
|
+
require "webvas"
|
|
50
61
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
circle 240, 160, 48
|
|
62
|
+
wgsl = RLSL.to_wgsl(:plasma) do
|
|
63
|
+
uniforms { float :time }
|
|
64
|
+
fragment do |frag_coord, resolution, uniforms|
|
|
65
|
+
uv = frag_coord / resolution.y
|
|
66
|
+
vec3(sin(uv.x + uniforms.time), uv.y, 0.6)
|
|
57
67
|
end
|
|
58
68
|
end
|
|
69
|
+
Webvas.run_shader(Webvas::Shader.new(wgsl))
|
|
59
70
|
~~~
|
|
60
71
|
|
|
61
|
-
|
|
72
|
+
WebGPU requires a supported browser, secure context, and GPU adapter. The shader runner updates the `resolution` and `time` uniforms automatically.
|
|
62
73
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
74
|
+
## CLI
|
|
75
|
+
|
|
76
|
+
~~~sh
|
|
77
|
+
gem install webvas
|
|
78
|
+
webvas new my-sketch
|
|
79
|
+
cd my-sketch
|
|
80
|
+
webvas serve
|
|
81
|
+
webvas build -o dist
|
|
67
82
|
~~~
|
|
68
83
|
|
|
69
|
-
|
|
84
|
+
`serve` binds to `127.0.0.1:8000`, serves `.wasm` as `application/wasm`, and reloads when project files change. Use `--host`, `--port`, or `--root` to change its defaults. `build` writes a static site that loads the official runtime. To bundle a custom runtime, add `ruby_wasm` and `js` to the project's `Gemfile`, install it, then run `webvas build --runtime custom`; the project's bundle is passed to `rbwasm build`.
|
|
70
85
|
|
|
71
86
|
## Playground
|
|
72
87
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
The footer reports the recent frame rate and average synchronous callback time. It measures Ruby-side work through command submission; it does not include GPU execution time. See [the runtime measurements](docs/spikes.md) for the tested environment and limits.
|
|
76
|
-
|
|
77
|
-
### Run Ruby from an HTML page
|
|
88
|
+
Open the [Webvas playground](https://rbgfx.github.io/webvas/). Choose a mode and example, edit the Ruby, then press **Run** or **Ctrl/Cmd + Enter**. Each run starts a fresh worker and canvas. **Share** compresses the source with `CompressionStream` into the URL fragment; **Download HTML** embeds the source as escaped JSON and keeps the runtime on its CDN URL.
|
|
78
89
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
~~~html
|
|
82
|
-
<canvas id="screen" width="320" height="240"></canvas>
|
|
83
|
-
<script type="text/ruby" data-webvas>
|
|
84
|
-
require "gesso"
|
|
85
|
-
Gesso.run(width: 320, height: 240, runner: :web) do
|
|
86
|
-
draw { background "#123456" }
|
|
87
|
-
end
|
|
88
|
-
</script>
|
|
89
|
-
<script src="https://rbgfx.github.io/webvas/loader.js"></script>
|
|
90
|
-
~~~
|
|
90
|
+
## Security and limitations
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
- Ruby runs in a worker, but the worker is not a security sandbox. Sketches can call browser APIs and make network requests allowed by the page's Content Security Policy. Inspect shared code before running it.
|
|
93
|
+
- Webvas does not set cookies, store source, or send code to a Webvas service. The GitHub Pages project URL shares the `rbgfx.github.io` origin; a dedicated hostname requires a separate hosting or domain decision.
|
|
94
|
+
- Runtime downloads omit browser credentials. Pages need to allow the chosen loader, worker, and runtime in their Content Security Policy.
|
|
95
|
+
- The `js` gem evaluates Ruby-to-JavaScript bridge code, so the playground policy requires `unsafe-eval` and `wasm-unsafe-eval`. This weakens script restrictions; use a dedicated origin for untrusted sketches.
|
|
96
|
+
- The 0.3.0 candidate was verified locally in Chromium 153 on macOS arm64. Firefox, Safari, and mobile browsers are not verified; the software renderer needs `OffscreenCanvas` and module workers, and RLSL also needs WebGPU.
|
|
97
|
+
- Blocking loops and `sleep` stop browser frame progress. WebAssembly threads and compiling native extensions in the browser are unsupported.
|
|
98
|
+
- Ruby source is limited to 32 KiB. RGBA frames use base64 transfer; a 320×240 frame is 307,200 bytes before encoding.
|
|
99
|
+
- RLSL mode requires WebGPU; RBGL and Gesso modes use the software renderer.
|
|
93
100
|
|
|
94
|
-
|
|
101
|
+
See [browser security notes](docs/security.md) and [runtime/performance records](docs/performance.md).
|
|
95
102
|
|
|
96
|
-
|
|
103
|
+
## Development
|
|
97
104
|
|
|
98
|
-
|
|
105
|
+
The repository expects adjacent checkouts of Larb, RBGL, Tessel, Gesso, Glyphic, and RLSL. Ruby unit tests run under CRuby; the browser integration tests use Chromium and the locally built runtime.
|
|
99
106
|
|
|
100
107
|
~~~sh
|
|
101
108
|
bundle install
|
|
102
109
|
BUNDLE_GEMFILE=runtime/Gemfile bundle install
|
|
103
|
-
mkdir -p build
|
|
104
110
|
BUNDLE_GEMFILE=runtime/Gemfile bundle exec rbwasm build \
|
|
105
111
|
--ruby-version 4.0 --target wasm32-unknown-wasip1 \
|
|
106
112
|
--build-profile full --patch "$(pwd)/patches/psych-wasi.patch" -o build/webvas.wasm
|
|
107
113
|
ruby script/build_site
|
|
108
|
-
ruby exe/webvas serve public
|
|
109
|
-
~~~
|
|
110
|
-
|
|
111
|
-
The runtime Gemfile uses the adjacent rbgfx source checkouts because Larb contains a native extension that the WASI build must compile. The build prints raw and deflate sizes so the deployed payload can be checked.
|
|
112
|
-
|
|
113
|
-
## Requirements
|
|
114
|
-
|
|
115
|
-
- CRuby 3.1 or later for the gem and CLI.
|
|
116
|
-
- WebAssembly, module workers, and <code>OffscreenCanvas</code> for the playground.
|
|
117
|
-
- A secure context for WebGPU. GPU support depends on the browser and device.
|
|
118
|
-
|
|
119
|
-
The browser worker does not expose the page DOM or browser storage to Ruby. Source typed in the editor is passed to the worker with a message; a project build can explicitly load a same-site sketch with the <code>?source=sketch.rb</code> query. Share links include compressed source in the URL fragment.
|
|
120
|
-
|
|
121
|
-
## Development
|
|
122
|
-
|
|
123
|
-
~~~sh
|
|
124
114
|
bundle exec rake verify
|
|
115
|
+
npm ci
|
|
116
|
+
npx playwright install chromium
|
|
117
|
+
npm run test:browser
|
|
125
118
|
~~~
|
|
126
119
|
|
|
127
|
-
This runs RuboCop, the test-unit suite with an 85% coverage floor, RBS validation, and JavaScript syntax checks. The Pages workflow builds the WASI runtime and deploys the static editor.
|
|
128
|
-
|
|
129
120
|
## License
|
|
130
121
|
|
|
131
122
|
MIT. See [LICENSE.txt](LICENSE.txt).
|
data/exe/webvas
CHANGED
|
@@ -1,271 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
require "
|
|
5
|
-
|
|
6
|
-
require "rbconfig"
|
|
7
|
-
require "socket"
|
|
8
|
-
require "stringio"
|
|
9
|
-
require "uri"
|
|
10
|
-
require "time"
|
|
11
|
-
require "zlib"
|
|
12
|
-
require_relative "../lib/webvas/version"
|
|
4
|
+
require "webvas"
|
|
5
|
+
require_relative "../lib/webvas/cli"
|
|
13
6
|
|
|
14
|
-
|
|
15
|
-
module CLI
|
|
16
|
-
STARTER = <<~'RUBY'
|
|
17
|
-
require "gesso"
|
|
18
|
-
|
|
19
|
-
Gesso.run(width: 480, height: 320, runner: :web) do
|
|
20
|
-
background "#171a18"
|
|
21
|
-
draw do
|
|
22
|
-
background "#171a18"
|
|
23
|
-
fill "#d38a62"
|
|
24
|
-
circle 240, 160, 48
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
RUBY
|
|
28
|
-
PLAYGROUND_URL = "https://rbgfx.github.io/webvas"
|
|
29
|
-
RUNTIME_URL = "#{PLAYGROUND_URL}/assets/webvas.wasm"
|
|
30
|
-
RELOAD_SCRIPT = <<~'JS'.freeze
|
|
31
|
-
(() => {
|
|
32
|
-
const directory = new URL(".", location.href);
|
|
33
|
-
const source = new URLSearchParams(location.search).get("source");
|
|
34
|
-
const sourceUrl = source && new URL(source, location.href);
|
|
35
|
-
const targets = [location.pathname];
|
|
36
|
-
if (sourceUrl && sourceUrl.origin === location.origin && sourceUrl.pathname.startsWith(directory.pathname)) {
|
|
37
|
-
targets.push(sourceUrl.pathname);
|
|
38
|
-
}
|
|
39
|
-
let previous;
|
|
40
|
-
setInterval(async () => {
|
|
41
|
-
const values = await Promise.all(targets.map(async path => {
|
|
42
|
-
try {
|
|
43
|
-
const response = await fetch(path, { method: "HEAD", cache: "no-store" });
|
|
44
|
-
return response.headers.get("ETag");
|
|
45
|
-
} catch {
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
}));
|
|
49
|
-
const current = values.join("|");
|
|
50
|
-
if (previous && current !== previous) location.reload();
|
|
51
|
-
previous = current;
|
|
52
|
-
}, 750);
|
|
53
|
-
})();
|
|
54
|
-
JS
|
|
55
|
-
|
|
56
|
-
module_function
|
|
57
|
-
|
|
58
|
-
def run(arguments)
|
|
59
|
-
case arguments.shift
|
|
60
|
-
when "new" then create(arguments.shift || abort("Usage: webvas new PATH"))
|
|
61
|
-
when "build" then build(arguments)
|
|
62
|
-
when "serve" then serve(arguments)
|
|
63
|
-
when "--version", "-v" then puts Webvas::VERSION
|
|
64
|
-
else
|
|
65
|
-
abort "Usage: webvas {new PATH|build [PROJECT] [-o DIR] [--runtime cdn|custom]|serve [DIRECTORY] [--port PORT]}"
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def create(path)
|
|
70
|
-
project = File.expand_path(path)
|
|
71
|
-
index = File.read(File.expand_path("../site/index.html", __dir__))
|
|
72
|
-
index.gsub!('href="./style.css"', "href=\"#{PLAYGROUND_URL}/style.css\"")
|
|
73
|
-
index.gsub!('src="./app.js"', "src=\"#{PLAYGROUND_URL}/app.js\"")
|
|
74
|
-
index.gsub!('content="./assets/webvas.wasm"', "content=\"#{RUNTIME_URL}\"")
|
|
75
|
-
index.sub!("<title>Webvas — Ruby, in motion</title>", "<title>#{CGI.escapeHTML(File.basename(project))} · Webvas</title>")
|
|
76
|
-
files = {
|
|
77
|
-
"index.html" => index,
|
|
78
|
-
"app.rb" => STARTER,
|
|
79
|
-
"Gemfile" => <<~GEMFILE,
|
|
80
|
-
source "https://rubygems.org"
|
|
81
|
-
|
|
82
|
-
gem "gesso"
|
|
83
|
-
gem "webvas", git: "https://github.com/rbgfx/webvas"
|
|
84
|
-
gem "ruby_wasm", "~> 2.10.1"
|
|
85
|
-
gem "js", "~> 2.10.1"
|
|
86
|
-
GEMFILE
|
|
87
|
-
"README.md" => <<~README,
|
|
88
|
-
# #{File.basename(project)}
|
|
89
|
-
|
|
90
|
-
Edit app.rb, then serve the project with Webvas:
|
|
91
|
-
|
|
92
|
-
webvas serve
|
|
93
|
-
|
|
94
|
-
Open http://127.0.0.1:8000/?source=app.rb. Build a static site with `webvas build -o dist/`.
|
|
95
|
-
To compile a custom runtime, add gems to the Gemfile, run `bundle install`, then use
|
|
96
|
-
`webvas build --runtime custom -o dist/`.
|
|
97
|
-
README
|
|
98
|
-
".gitignore" => "/.webvas/\n/public/\n"
|
|
99
|
-
}
|
|
100
|
-
existing = files.keys.select { |name| File.exist?(File.join(project, name)) }
|
|
101
|
-
abort "Already exists: #{existing.join(", ")}" unless existing.empty?
|
|
102
|
-
|
|
103
|
-
FileUtils.mkdir_p(project)
|
|
104
|
-
files.each { |name, contents| File.write(File.join(project, name), contents) }
|
|
105
|
-
puts "Created #{project}/index.html and #{project}/app.rb"
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def build(arguments)
|
|
109
|
-
project = Dir.pwd
|
|
110
|
-
project_argument_seen = false
|
|
111
|
-
destination = nil
|
|
112
|
-
runtime = "cdn"
|
|
113
|
-
until arguments.empty?
|
|
114
|
-
argument = arguments.shift
|
|
115
|
-
case argument
|
|
116
|
-
when "-o", "--output" then destination = arguments.shift || abort("Missing path after #{argument}")
|
|
117
|
-
when "--runtime" then runtime = arguments.shift || abort("Missing runtime after --runtime")
|
|
118
|
-
when "--help", "-h" then abort "Usage: webvas build [PROJECT] [-o DIRECTORY] [--runtime cdn|custom]"
|
|
119
|
-
else
|
|
120
|
-
if !project_argument_seen && !argument.start_with?("-")
|
|
121
|
-
project = File.expand_path(argument)
|
|
122
|
-
project_argument_seen = true
|
|
123
|
-
else
|
|
124
|
-
abort "Unexpected argument: #{argument}"
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
|
-
abort "Runtime must be cdn or custom" unless %w[cdn custom].include?(runtime)
|
|
129
|
-
|
|
130
|
-
root = File.expand_path("..", __dir__)
|
|
131
|
-
destination = File.expand_path(destination || "public", project)
|
|
132
|
-
wasm = nil
|
|
133
|
-
runtime_url = RUNTIME_URL
|
|
134
|
-
if runtime == "custom"
|
|
135
|
-
wasm = File.join(project, ".webvas", "build", "webvas.wasm")
|
|
136
|
-
gemfile = File.join(project, "Gemfile")
|
|
137
|
-
runtime_gemfile = File.join(project, "runtime", "Gemfile")
|
|
138
|
-
project_gemfile = File.file?(gemfile) && File.read(gemfile).match?(/^\s*gem\s+["']ruby_wasm["']/)
|
|
139
|
-
gemfile = if File.file?(runtime_gemfile)
|
|
140
|
-
runtime_gemfile
|
|
141
|
-
elsif project_gemfile
|
|
142
|
-
gemfile
|
|
143
|
-
else
|
|
144
|
-
File.join(root, "runtime", "Gemfile")
|
|
145
|
-
end
|
|
146
|
-
FileUtils.mkdir_p(File.dirname(wasm))
|
|
147
|
-
env = { "BUNDLE_GEMFILE" => gemfile }
|
|
148
|
-
success = system(env, "bundle", "exec", "rbwasm", "build", "--ruby-version", "4.0",
|
|
149
|
-
"--target", "wasm32-unknown-wasip1", "--build-profile", "full",
|
|
150
|
-
"--patch", File.join(root, "patches", "psych-wasi.patch"), "-o", wasm,
|
|
151
|
-
chdir: project)
|
|
152
|
-
abort "ruby.wasm build failed" unless success
|
|
153
|
-
runtime_url = "./assets/webvas.wasm"
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
command = [RbConfig.ruby, File.join(root, "script", "build_site"), destination, runtime_url]
|
|
157
|
-
command << wasm if wasm
|
|
158
|
-
success = system(*command)
|
|
159
|
-
abort "Playground build failed" unless success
|
|
160
|
-
app = File.join(project, "app.rb")
|
|
161
|
-
FileUtils.cp(app, File.join(destination, "app.rb")) if File.file?(app)
|
|
162
|
-
puts "Open http://127.0.0.1:8000/?source=app.rb" if File.file?(app)
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
def serve(arguments)
|
|
166
|
-
port = 8000
|
|
167
|
-
directory = nil
|
|
168
|
-
until arguments.empty?
|
|
169
|
-
argument = arguments.shift
|
|
170
|
-
if argument == "--port"
|
|
171
|
-
port = Integer(arguments.shift || abort("Missing port after --port"))
|
|
172
|
-
elsif directory.nil?
|
|
173
|
-
directory = argument
|
|
174
|
-
else
|
|
175
|
-
abort "Usage: webvas serve [DIRECTORY] [--port PORT]"
|
|
176
|
-
end
|
|
177
|
-
end
|
|
178
|
-
abort "Port must be between 1 and 65535" unless port.between?(1, 65_535)
|
|
179
|
-
|
|
180
|
-
serve_static(File.expand_path(directory || Dir.pwd), port)
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
def serve_static(directory, port)
|
|
184
|
-
root = File.realpath(directory)
|
|
185
|
-
server = TCPServer.new("127.0.0.1", port)
|
|
186
|
-
puts "Serving #{root} at http://127.0.0.1:#{port}/"
|
|
187
|
-
loop do
|
|
188
|
-
client = server.accept
|
|
189
|
-
Thread.new(client, root) { |socket, base| handle_request(socket, base) }
|
|
190
|
-
end
|
|
191
|
-
ensure
|
|
192
|
-
server&.close
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
def handle_request(socket, root)
|
|
196
|
-
request = socket.gets("\n", 8192)&.split
|
|
197
|
-
return unless request
|
|
198
|
-
|
|
199
|
-
method, target = request
|
|
200
|
-
headers = {}
|
|
201
|
-
while (line = socket.gets("\n", 8192)) && line != "\r\n" && line != "\n"
|
|
202
|
-
name, value = line.split(":", 2)
|
|
203
|
-
headers[name.downcase] = value.to_s.strip if value
|
|
204
|
-
end
|
|
205
|
-
unless %w[GET HEAD].include?(method)
|
|
206
|
-
return response(socket, 405, "Method not allowed", "text/plain", "Method not allowed")
|
|
207
|
-
end
|
|
208
|
-
path = URI::RFC2396_Parser.new.unescape(URI.parse(target).path)
|
|
209
|
-
if path == "/__webvas_reload.js"
|
|
210
|
-
return response(socket, 200, "OK", "text/javascript", RELOAD_SCRIPT, "Cache-Control" => "no-store")
|
|
211
|
-
end
|
|
212
|
-
candidate = File.realpath(File.join(root, path.delete_prefix("/")))
|
|
213
|
-
candidate = File.realpath(File.join(candidate, "index.html")) if File.directory?(candidate)
|
|
214
|
-
unless candidate.start_with?("#{root}/") || candidate == root
|
|
215
|
-
return response(socket, 404, "Not found", "text/plain", "Not found")
|
|
216
|
-
end
|
|
217
|
-
return response(socket, 404, "Not found", "text/plain", "Not found") unless File.file?(candidate)
|
|
218
|
-
|
|
219
|
-
type = { ".css" => "text/css", ".html" => "text/html", ".js" => "text/javascript",
|
|
220
|
-
".json" => "application/json", ".wasm" => "application/wasm", ".rb" => "text/plain" }
|
|
221
|
-
content_type = type.fetch(File.extname(candidate), "application/octet-stream")
|
|
222
|
-
content_type = "#{content_type}; charset=utf-8" if content_type.start_with?("text/", "application/json")
|
|
223
|
-
body = File.binread(candidate)
|
|
224
|
-
if content_type.start_with?("text/html")
|
|
225
|
-
hook = '<script src="/__webvas_reload.js" defer></script>'
|
|
226
|
-
body.sub!(%r{</body>}i) { |closing| "#{hook}#{closing}" } || body.concat(hook)
|
|
227
|
-
end
|
|
228
|
-
response_headers = {
|
|
229
|
-
"Content-Type" => content_type,
|
|
230
|
-
"Last-Modified" => File.mtime(candidate).httpdate,
|
|
231
|
-
"ETag" => %Q("#{File.size(candidate).to_s(16)}-#{File.mtime(candidate).to_f}"),
|
|
232
|
-
"Cache-Control" => File.extname(candidate) == ".wasm" ? "public, max-age=86400" : "no-store",
|
|
233
|
-
"X-Content-Type-Options" => "nosniff"
|
|
234
|
-
}
|
|
235
|
-
response_headers["Vary"] = "Accept-Encoding" if headers.fetch("accept-encoding", "").include?("gzip")
|
|
236
|
-
if gzip_accepted?(headers) && body.bytesize > 1024
|
|
237
|
-
buffer = StringIO.new("".b)
|
|
238
|
-
Zlib::GzipWriter.wrap(buffer) { |gzip| gzip.write(body) }
|
|
239
|
-
body = buffer.string
|
|
240
|
-
response_headers["Content-Encoding"] = "gzip"
|
|
241
|
-
end
|
|
242
|
-
response_headers["Content-Length"] = body.bytesize
|
|
243
|
-
socket.write("HTTP/1.1 200 OK\r\n#{response_headers.map { |name, value| "#{name}: #{value}" }.join("\r\n")}\r\nConnection: close\r\n\r\n")
|
|
244
|
-
socket.write(body) unless method == "HEAD"
|
|
245
|
-
rescue Errno::EPIPE, Errno::ECONNRESET, Errno::ENOTCONN, Errno::ECONNABORTED
|
|
246
|
-
nil
|
|
247
|
-
rescue ArgumentError, Errno::ENOENT, URI::InvalidURIError
|
|
248
|
-
response(socket, 404, "Not found", "text/plain", "Not found")
|
|
249
|
-
ensure
|
|
250
|
-
socket.close
|
|
251
|
-
end
|
|
252
|
-
|
|
253
|
-
def response(socket, code, reason, type, body, headers = {})
|
|
254
|
-
response_headers = {
|
|
255
|
-
"Content-Type" => "#{type}; charset=utf-8",
|
|
256
|
-
"Content-Length" => body.bytesize,
|
|
257
|
-
"Connection" => "close"
|
|
258
|
-
}.merge(headers)
|
|
259
|
-
socket.write("HTTP/1.1 #{code} #{reason}\r\n#{response_headers.map { |name, value| "#{name}: #{value}" }.join("\r\n")}\r\n\r\n#{body}")
|
|
260
|
-
end
|
|
261
|
-
|
|
262
|
-
def gzip_accepted?(headers)
|
|
263
|
-
headers.fetch("accept-encoding", "").split(",").any? do |encoding|
|
|
264
|
-
name, *parameters = encoding.strip.split(";")
|
|
265
|
-
name == "gzip" && parameters.none? { |parameter| parameter.strip.match?(/\Aq=0(?:\.0*)?\z/) }
|
|
266
|
-
end
|
|
267
|
-
end
|
|
268
|
-
end
|
|
269
|
-
end
|
|
270
|
-
|
|
271
|
-
Webvas::CLI.run(ARGV) if $PROGRAM_NAME == __FILE__
|
|
7
|
+
exit Webvas::CLI.run
|