webvas 0.3.0 → 0.3.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e74011a1aa90c2b4a7b339e3ccb0660033e375a6f5b37199c1b429e038af5f77
4
- data.tar.gz: 9d6e7f7dca48db357712fced13ad4e9e07afd1742208136294feed9b26eb93bd
3
+ metadata.gz: 23e89d8e5afb08ea67551ec3be09d3481ba59795bb3580f3cde32c180a06f4c3
4
+ data.tar.gz: fb7dc400c269810d05ea2ad5ac186d8571bd54e521c0dc12d4aa169e674561e6
5
5
  SHA512:
6
- metadata.gz: f0b27578c68b7bca6eb81a832732739c90b22835cf1e40b4b0619614a8f1e6de5f04b72b4f75e5b5f0e184c760977387ad9ee479a5fec8d537a25994dab5cc1d
7
- data.tar.gz: e56a688ab162568fe5ada3a19863e46d6f3ad98b4a61071c48e07ab7d6736c792edaa8a551e849c8e91cdf4f8574e76fa3d2631cd8c5aa1d979da9e83756f7b0
6
+ metadata.gz: 8c4d31e781969e8bf9bd441e655e794198fd9eff6ac79f80dfdc663dc82629d1d93b974bf13ba12a9fb825c0c96cd60ff71cbe139c0431165213ac46b99068bb
7
+ data.tar.gz: 23c452946ffeef36cde2e5dadbd6383b888419aed2dd52aa90be66e6294cf1ee94476ebfb0f41c9ed93acdb6b74016e42e3dc6281245faeb1a6eb543ad8d4620
data/CHANGELOG.md CHANGED
@@ -1,11 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.2] - 2026-09-26
4
+
5
+ ### Fixed
6
+
7
+ - Reject build output paths that resolve through symlinks into the project or its assets.
8
+
9
+ ## [0.3.1] - 2026-09-25
10
+
11
+ ### Fixed
12
+
13
+ - Include the `webvas` command and runtime build files in the installed gem.
14
+ - Use the current WebAssembly runtime on the published playground.
15
+ - Install a Gesso version that supports browser sketches.
16
+
3
17
  ## [0.3.0] - 2026-09-25
4
18
 
5
19
  ### Added
6
20
 
7
- - Add the RBGL browser backend, pointer and keyboard input, and Gesso runner.
8
- - Add RLSL WebGPU rendering and the shared Glaze shader runner.
9
- - Add a nine-example playground, compressed sharing, source highlighting on errors, and HTML export.
10
- - Add `webvas new`, `serve`, and `build` commands with gzip and live reload.
11
- - Report Ruby callback frame timing and runtime size measurements.
21
+ - Gesso sketches and RLSL WGSL shaders in the browser runtime.
22
+ - An editable playground with example modes, compressed share URLs, and standalone HTML downloads.
23
+ - `webvas new`, `serve`, and `build` commands, including custom WebAssembly runtime builds.
24
+ - Worker reset between runs, Ruby error reporting, and frame performance measurements.
25
+
26
+ ## [0.1.0] - 2026-09-25
27
+
28
+ Initial release
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2026 ydah
3
+ Copyright (c) 2026 Yudai Takada
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -2,130 +2,121 @@
2
2
 
3
3
  # Webvas
4
4
 
5
- **A small drawing machine for Ruby.**
5
+ **Write Ruby graphics sketches and run them in a browser.**
6
6
 
7
- Run RBGL and Gesso sketches in a browser with CRuby compiled to WebAssembly.
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
- ## What it does
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
- Webvas connects RBGL's framebuffer to an <code>OffscreenCanvas</code>, schedules frames on a worker, and maps browser pointer and keyboard input to RBGL events. Gesso can use the same backend with <code>runner: :web</code>; Glaze shaders use the shared WebGPU runner.
13
+ ## Features
16
14
 
17
- The public playground runs user Ruby in a dedicated worker. The worker has no document or storage APIs; the page transfers only the canvases and input events. It receives the edited source through <code>postMessage</code>.
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
- ## Install
22
+ ## Quick start
20
23
 
21
- The `0.3.0` gem is not on RubyGems yet. Add the repository to a Gemfile:
24
+ Add the loader and a Ruby source file to an HTML page:
22
25
 
23
- ~~~ruby
24
- gem "webvas", git: "https://github.com/rbgfx/webvas"
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
- Gesso is an optional companion. Add <code>gem "gesso"</code> if you want its 2D sketch DSL.
28
-
29
- ## Ruby API
33
+ In `app.rb`:
30
34
 
31
35
  ~~~ruby
32
- require "webvas"
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
- Webvas.run(window) do |context, delta_time|
39
- context.clear
40
- # Bind an RBGL pipeline and draw here.
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
- <code>Webvas.run</code> uses one reusable <code>Proc</code> with <code>requestAnimationFrame</code>. The callback receives RBGL's context and the elapsed seconds. Call <code>window.stop</code> to end the loop.
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
- Gesso's browser runner is selected per sketch:
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 "gesso"
59
+ require "rlsl"
60
+ require "webvas"
50
61
 
51
- Gesso.run(width: 480, height: 320, runner: :web, pixelated: false) do
52
- background "#171a18"
53
- draw do
54
- background "#171a18"
55
- fill "#d38a62"
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
- To run a WGSL shader, provide its generated source and uniform layout:
72
+ WebGPU requires a supported browser, secure context, and GPU adapter. The shader runner updates the `resolution` and `time` uniforms automatically.
62
73
 
63
- ~~~ruby
64
- shader = Webvas::Shader.new(wgsl_source, layout: uniform_layout)
65
- shader.run { |time| { gain: 0.75, time: time } }
66
- # shader.stop when it is no longer needed
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
- The WebGPU helper is shared with Glaze's standalone HTML exporter. WebGPU is optional; browsers without a usable adapter report an error in the playground.
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
- The editor supports examples, Ctrl/⌘ + Enter, compressed share links, and Ruby or standalone HTML downloads. The HTML includes the sketch and UI; it loads the runtime and playground assets from the Webvas Pages site. The source limit for a run or share link is 32 KiB.
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
- Add a canvas, a Ruby script, and the loader. The loader defaults to `#screen`, reads the official WASM runtime beside itself, and reports startup or runtime errors in an `<output>` element. Set `data-canvas` on the Ruby script and pass the same selector to `Webvas::Backend` or Gesso when the canvas has another ID.
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
- See the [loader example](https://rbgfx.github.io/webvas/loader-example.html) for pointer input.
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
- ## CLI
101
+ See [browser security notes](docs/security.md) and [runtime/performance records](docs/performance.md).
95
102
 
96
- `webvas new mysketch` creates an `index.html` and `app.rb` that use the official runtime from Webvas Pages. Run `webvas serve` in that directory for gzip and live reload, or export it with `webvas build -o dist/`. The default build keeps the runtime on the CDN. Add gems to the project bundle and use `webvas build --runtime custom -o dist/` to compile a custom WASM runtime.
103
+ ## Development
97
104
 
98
- To compile and serve the playground from this checkout:
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 "fileutils"
5
- require "cgi/escape"
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
- module Webvas
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