dragonruby-rack 0.1.0
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 +7 -0
- data/CHANGELOG.md +19 -0
- data/LICENSE.txt +21 -0
- data/README.md +105 -0
- data/lib/dragonruby/rack/bundle.rb +86 -0
- data/lib/dragonruby/rack/game.rb +94 -0
- data/lib/dragonruby/rack/railtie.rb +21 -0
- data/lib/dragonruby/rack/version.rb +5 -0
- data/lib/dragonruby/rack.rb +13 -0
- data/lib/dragonruby-rack.rb +3 -0
- data/lib/generators/dragonruby/mount/mount_generator.rb +82 -0
- data/lib/tasks/dragonruby.rake +20 -0
- metadata +78 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 4eb445a0e6d8d213740fb5604c1dbd3794d92732a540d5157837fee9f22a4314
|
|
4
|
+
data.tar.gz: 4957d3f7413f65f43ee11f236cb8bfe892a4b7aac958493837eb8985b79219c0
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: fd9e975ed8ef7a749e9f67a8f14db04b4c8fe7ff621097977471197a40fc848d5317ee76656ad677f3f7c3362fd31975529189d4ad410fc76211b2d9e63d4534
|
|
7
|
+
data.tar.gz: b0763b9f6ac8f1ffe3bc16d015a8a113dcb1160d88df5d62903351d05cd3faa5ab9fb77492ba5298ce1fc233bddc7fb69ac5ad75352030a1af281ed9537a85c9
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 — 2026-08-02
|
|
4
|
+
|
|
5
|
+
Extracted from https://stammer.dev website, where it had been serving
|
|
6
|
+
[Underwater](https://github.com/rstammer/underwater) inside of a classical Rails app.
|
|
7
|
+
|
|
8
|
+
- `DragonRuby::Rack::Game` — mountable endpoint: cross-origin isolation headers
|
|
9
|
+
on the document and every subresource, `<base>` injection so relative assets
|
|
10
|
+
resolve under any mount path, `content-length` corrected for the longer body.
|
|
11
|
+
- A bare mount path (empty `PATH_INFO`, which is what `Rack::URLMap` hands a
|
|
12
|
+
mounted app) now serves the index. It used to 404 in plain Rack while working
|
|
13
|
+
under Rails, which normalizes the path first.
|
|
14
|
+
- `DragonRuby::Rack::Bundle.install` and `rake dragonruby:install` — replace the
|
|
15
|
+
bundle directory from a published zip, refusing any destination that is not
|
|
16
|
+
empty and holds no `index.html`, and verifying the result has an `index.html`
|
|
17
|
+
and a `.wasm`.
|
|
18
|
+
- `rails g dragonruby:mount` — writes the mount into `config/routes.rb`.
|
|
19
|
+
- Railtie loads only when Rails is present; Rails is not a dependency.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Robin Stammer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# dragonruby-rack
|
|
2
|
+
|
|
3
|
+
Serve a [DragonRuby GTK](https://dragonruby.org) HTML5 export from a Rack or
|
|
4
|
+
Rails app, on your own domain, without an iframe and without itch.io.
|
|
5
|
+
|
|
6
|
+
```ruby
|
|
7
|
+
# Gemfile
|
|
8
|
+
gem "dragonruby-rack"
|
|
9
|
+
|
|
10
|
+
# config/routes.rb
|
|
11
|
+
mount DragonRuby::Rack::Game.new(Rails.root.join("games/underwater")) => "/underwater"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
That is the whole thing. The game is then at `/underwater/`.
|
|
15
|
+
|
|
16
|
+
Built for and running on [stammer.dev/underwater](https://stammer.dev/underwater/).
|
|
17
|
+
|
|
18
|
+
## Why this is not just a static directory
|
|
19
|
+
|
|
20
|
+
DragonRuby's WASM runtime allocates a `SharedArrayBuffer`, and browsers hand
|
|
21
|
+
that out only to a **cross-origin isolated** document — one served with both
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
Cross-Origin-Opener-Policy: same-origin
|
|
25
|
+
Cross-Origin-Embedder-Policy: require-corp
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Rails' static file server runs *before* the router and cannot set headers per
|
|
29
|
+
path, so a bundle dropped into `public/` is served without them and the game
|
|
30
|
+
never boots. Hence: keep the bundle outside `public/`, serve it through this
|
|
31
|
+
endpoint, which sets the headers on the document **and on every subresource** —
|
|
32
|
+
`require-corp` applies to the `.wasm` too.
|
|
33
|
+
|
|
34
|
+
The second thing it does is less obvious. The export links its assets
|
|
35
|
+
relatively (`game.css`, `gamedata/…`), which under a mount only resolve if the
|
|
36
|
+
document URL ends in a slash — and nothing can guarantee that, since Rails
|
|
37
|
+
normalizes the path away before the endpoint sees it and a redirect onto the
|
|
38
|
+
slash would be a loop. So the endpoint injects a `<base href="/underwater/">`
|
|
39
|
+
into the served HTML, and the assets resolve either way.
|
|
40
|
+
|
|
41
|
+
## Without Rails
|
|
42
|
+
|
|
43
|
+
It is a plain Rack app; Rails is not a dependency.
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
# config.ru
|
|
47
|
+
require "dragonruby-rack"
|
|
48
|
+
run DragonRuby::Rack::Game.new("builds/underwater-html5")
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
or mounted:
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
require "dragonruby-rack"
|
|
55
|
+
|
|
56
|
+
app = Rack::Builder.new do
|
|
57
|
+
map("/underwater") { run DragonRuby::Rack::Game.new("builds/underwater-html5") }
|
|
58
|
+
end
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Getting a bundle in
|
|
62
|
+
|
|
63
|
+
Export the game (`dragonruby-publish --platforms=html5 --package …`) and unpack
|
|
64
|
+
the zip where the mount points:
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
rake dragonruby:install[../underwater/builds/underwater-html5.zip,underwater]
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The task **replaces** the directory rather than unpacking over it — a file that
|
|
71
|
+
existed in the old version and not in the new one would otherwise stay behind
|
|
72
|
+
and be served for good. Because that is a delete, it refuses any destination
|
|
73
|
+
that is not empty and holds no `index.html`, and it checks afterwards that what
|
|
74
|
+
came out has an `index.html` and a `.wasm` in it (a desktop export unzips
|
|
75
|
+
perfectly well and then serves nothing). `FORCE=1` overrides the refusal.
|
|
76
|
+
|
|
77
|
+
`config.dragonruby.games_root` sets where bundles live; the default is `games`.
|
|
78
|
+
|
|
79
|
+
## Generator
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
rails g dragonruby:mount underwater
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Creates `games/underwater/` and writes the mount into `config/routes.rb`.
|
|
86
|
+
`--path=` and `--at=` override the directory and the URL. Running it twice does
|
|
87
|
+
not mount the path twice.
|
|
88
|
+
|
|
89
|
+
## What this gem is not
|
|
90
|
+
|
|
91
|
+
It does **not** contain DragonRuby, and it does not build your game. DragonRuby
|
|
92
|
+
is commercial software with its own licence; you need your own copy to produce
|
|
93
|
+
the export this serves. This gem only takes the finished directory and puts it
|
|
94
|
+
on the web correctly.
|
|
95
|
+
|
|
96
|
+
## Development
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
bundle install
|
|
100
|
+
bundle exec rspec
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Licence
|
|
104
|
+
|
|
105
|
+
MIT. See LICENSE.txt.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
|
|
3
|
+
module DragonRuby
|
|
4
|
+
module Rack
|
|
5
|
+
# Puts a published HTML5 export where the app can serve it.
|
|
6
|
+
#
|
|
7
|
+
# This is the step that used to be three shell lines, one of which was
|
|
8
|
+
# `rm -rf` against a path typed by hand. Replacing the directory rather than
|
|
9
|
+
# unpacking over it is not optional — a file deleted between two versions
|
|
10
|
+
# would otherwise stay behind and be served forever. So the delete has to
|
|
11
|
+
# happen, and the only useful thing to do about it is to refuse to delete
|
|
12
|
+
# anything that is not already a bundle.
|
|
13
|
+
module Bundle
|
|
14
|
+
class Error < StandardError; end
|
|
15
|
+
|
|
16
|
+
# What every DragonRuby HTML5 export has in it, and what a directory must
|
|
17
|
+
# therefore look like before this is willing to erase it.
|
|
18
|
+
MARKER = "index.html"
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
# Unpacks +zip+ into +into+, replacing whatever bundle is there.
|
|
22
|
+
#
|
|
23
|
+
# Refuses when +into+ exists but holds no index.html: that is either the
|
|
24
|
+
# wrong path or something that is not a bundle, and both are worth an
|
|
25
|
+
# error rather than a deletion. Pass force: true to say you meant it.
|
|
26
|
+
def install(zip, into, force: false)
|
|
27
|
+
zip = File.expand_path(zip.to_s)
|
|
28
|
+
into = File.expand_path(into.to_s)
|
|
29
|
+
|
|
30
|
+
raise Error, "no such zip: #{zip}" unless File.file?(zip)
|
|
31
|
+
|
|
32
|
+
guard_destination(into, force: force)
|
|
33
|
+
|
|
34
|
+
FileUtils.rm_rf(into)
|
|
35
|
+
FileUtils.mkdir_p(into)
|
|
36
|
+
unzip(zip, into)
|
|
37
|
+
verify(into)
|
|
38
|
+
|
|
39
|
+
into
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def guard_destination(into, force:)
|
|
45
|
+
return if force
|
|
46
|
+
return unless File.exist?(into)
|
|
47
|
+
|
|
48
|
+
unless File.directory?(into)
|
|
49
|
+
raise Error, "#{into} exists and is not a directory"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
return if File.exist?(File.join(into, MARKER))
|
|
53
|
+
return if Dir.children(into).empty?
|
|
54
|
+
|
|
55
|
+
raise Error, <<~MESSAGE.strip
|
|
56
|
+
#{into} is not empty and holds no #{MARKER}, so it does not look like
|
|
57
|
+
a game bundle. Refusing to delete it. Pass force: true (or
|
|
58
|
+
FORCE=1 to the rake task) if this really is the right directory.
|
|
59
|
+
MESSAGE
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Shelling out to unzip keeps rubyzip out of the dependency list for a
|
|
63
|
+
# step that runs once per release on a developer machine. If it is ever
|
|
64
|
+
# needed somewhere without unzip, that is the moment to reconsider —
|
|
65
|
+
# not before.
|
|
66
|
+
def unzip(zip, into)
|
|
67
|
+
ok = system("unzip", "-q", zip, "-d", into)
|
|
68
|
+
raise Error, "unzip failed for #{zip}" unless ok
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# A zip that unpacked without error can still be the wrong zip. Both
|
|
72
|
+
# halves of a DragonRuby export have to be there or the game shows a
|
|
73
|
+
# blank page in production and nowhere else.
|
|
74
|
+
def verify(into)
|
|
75
|
+
unless File.exist?(File.join(into, MARKER))
|
|
76
|
+
raise Error, "no #{MARKER} in the unpacked bundle at #{into}"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
return if Dir.glob(File.join(into, "**", "*.wasm")).any?
|
|
80
|
+
|
|
81
|
+
raise Error, "no .wasm in the unpacked bundle at #{into} — is this an HTML5 export?"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
require "rack"
|
|
2
|
+
|
|
3
|
+
module DragonRuby
|
|
4
|
+
module Rack
|
|
5
|
+
# Serves a DragonRuby GTK HTML5 export as a self-contained, cross-origin
|
|
6
|
+
# isolated static bundle, mountable at any path in a Rack or Rails app:
|
|
7
|
+
#
|
|
8
|
+
# mount DragonRuby::Rack::Game.new("games/underwater") => "/underwater"
|
|
9
|
+
#
|
|
10
|
+
# Why this exists: DragonRuby's WASM runtime uses SharedArrayBuffer, which
|
|
11
|
+
# the browser only hands out to a *cross-origin isolated* document — one
|
|
12
|
+
# served with both COOP and COEP. Rails' public/ file server
|
|
13
|
+
# (ActionDispatch::Static) runs before the router and cannot set these
|
|
14
|
+
# headers per path, which is why the bundle belongs outside public/ and is
|
|
15
|
+
# served exclusively through this endpoint.
|
|
16
|
+
#
|
|
17
|
+
# Everything in here refers to the real Rack as ::Rack. Inside
|
|
18
|
+
# DragonRuby::Rack, a bare `Rack::Static` resolves to
|
|
19
|
+
# DragonRuby::Rack::Static and raises NameError — the one price of naming
|
|
20
|
+
# the module after the gem.
|
|
21
|
+
class Game
|
|
22
|
+
# The two headers that put the document into cross-origin isolation.
|
|
23
|
+
# Without them the WASM runtime cannot allocate its SharedArrayBuffer and
|
|
24
|
+
# refuses to boot. Same-origin subresources (all of the bundle) satisfy
|
|
25
|
+
# require-corp without a per-file Cross-Origin-Resource-Policy header.
|
|
26
|
+
ISOLATION_HEADERS = {
|
|
27
|
+
"cross-origin-opener-policy" => "same-origin",
|
|
28
|
+
"cross-origin-embedder-policy" => "require-corp",
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
NOT_FOUND = ->(_env) { [404, { "content-type" => "text/plain" }, ["Not found"]] }
|
|
32
|
+
|
|
33
|
+
attr_reader :root
|
|
34
|
+
|
|
35
|
+
# A missing directory is deliberately not an error. Routes are drawn at
|
|
36
|
+
# boot, and a fresh clone has no build in it yet; raising here would stop
|
|
37
|
+
# the whole app from starting over a game that has not been exported.
|
|
38
|
+
# Requests simply 404 until the bundle is there.
|
|
39
|
+
def initialize(root, index: "index.html")
|
|
40
|
+
@root = root.to_s
|
|
41
|
+
@static = ::Rack::Static.new(
|
|
42
|
+
NOT_FOUND,
|
|
43
|
+
root: @root,
|
|
44
|
+
urls: [""], # serve every path under this mount from the bundle
|
|
45
|
+
index: index, # directory requests ("/") resolve to index.html
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def call(env)
|
|
50
|
+
env = normalize_root(env)
|
|
51
|
+
status, headers, body = @static.call(env)
|
|
52
|
+
ISOLATION_HEADERS.each { |key, value| headers[key] = value }
|
|
53
|
+
body = rebase_html(body, headers, env) if html?(headers)
|
|
54
|
+
[status, headers, body]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
# A request for the mount point itself arrives with an empty PATH_INFO
|
|
60
|
+
# under Rack::URLMap, and Rack::Static only resolves its index for a path
|
|
61
|
+
# ending in "/". So a plain Rack mount answered the bare URL with 404
|
|
62
|
+
# while the same mount under Rails worked, because Rails normalizes the
|
|
63
|
+
# path to "/" before the endpoint sees it. Same request, same app, two
|
|
64
|
+
# answers — worth removing rather than documenting.
|
|
65
|
+
def normalize_root(env)
|
|
66
|
+
return env unless env["PATH_INFO"].to_s.empty?
|
|
67
|
+
|
|
68
|
+
env.merge("PATH_INFO" => "/")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def html?(headers)
|
|
72
|
+
headers["content-type"].to_s.include?("text/html")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# The bundle's index.html links its assets relatively (game.css, the
|
|
76
|
+
# loader, gamedata/). Mounted under a subpath, those only resolve if the
|
|
77
|
+
# document URL ends in a slash — which cannot be forced, since Rails
|
|
78
|
+
# normalizes the path away before the mount sees it, and a redirect onto
|
|
79
|
+
# the slash would be a loop. Injecting a <base> pointing at the mount root
|
|
80
|
+
# makes every relative URL resolve either way.
|
|
81
|
+
def rebase_html(body, headers, env)
|
|
82
|
+
html = +""
|
|
83
|
+
body.each { |chunk| html << chunk }
|
|
84
|
+
body.close if body.respond_to?(:close)
|
|
85
|
+
return [html] if html.empty? # HEAD carries no body — leave headers as-is
|
|
86
|
+
|
|
87
|
+
base = "#{env['SCRIPT_NAME']}/"
|
|
88
|
+
html = html.sub(/<head>/i, %(<head><base href="#{base}">))
|
|
89
|
+
headers["content-length"] = html.bytesize.to_s
|
|
90
|
+
[html]
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
require "rails/railtie"
|
|
2
|
+
|
|
3
|
+
module DragonRuby
|
|
4
|
+
module Rack
|
|
5
|
+
# Everything Rails-specific in the gem is here and in lib/generators. The
|
|
6
|
+
# railtie is loaded only when Rails is already in the process (see
|
|
7
|
+
# lib/dragonruby/rack.rb), so requiring this file is what makes the gem a
|
|
8
|
+
# Rails gem — nothing else in it knows about Rails at all.
|
|
9
|
+
class Railtie < ::Rails::Railtie
|
|
10
|
+
# config.dragonruby.games_root — where bundles live, relative to the app
|
|
11
|
+
# root. Only the rake task and the generator read it; the Rack app itself
|
|
12
|
+
# takes a plain path and has no configuration.
|
|
13
|
+
config.dragonruby = ActiveSupport::OrderedOptions.new
|
|
14
|
+
config.dragonruby.games_root = "games"
|
|
15
|
+
|
|
16
|
+
rake_tasks do
|
|
17
|
+
load File.expand_path("../../tasks/dragonruby.rake", __dir__)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
require_relative "rack/version"
|
|
2
|
+
require_relative "rack/game"
|
|
3
|
+
require_relative "rack/bundle"
|
|
4
|
+
|
|
5
|
+
# The Rails half is optional on purpose: the gem is a Rack gem, and a Sinatra or
|
|
6
|
+
# plain-Rack user should not pull a framework in behind it. Loading the railtie
|
|
7
|
+
# only when Rails is already present keeps that promise.
|
|
8
|
+
require_relative "rack/railtie" if defined?(::Rails::Railtie)
|
|
9
|
+
|
|
10
|
+
module DragonRuby
|
|
11
|
+
module Rack
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
|
|
3
|
+
module DragonRuby
|
|
4
|
+
module Generators
|
|
5
|
+
# rails g dragonruby:mount underwater
|
|
6
|
+
#
|
|
7
|
+
# Writes the one line that is easy to get subtly wrong: the mount has to
|
|
8
|
+
# point at a directory outside public/, and the game is linked with a
|
|
9
|
+
# trailing slash.
|
|
10
|
+
#
|
|
11
|
+
# The namespace is declared rather than derived. Rails would camelize
|
|
12
|
+
# "dragonruby" to Dragonruby and look for that constant; the gem's module is
|
|
13
|
+
# DragonRuby, so the two are spelled out separately here.
|
|
14
|
+
class MountGenerator < ::Rails::Generators::Base
|
|
15
|
+
namespace "dragonruby:mount"
|
|
16
|
+
|
|
17
|
+
argument :name, type: :string,
|
|
18
|
+
desc: "Name of the game — becomes both the directory and the URL path"
|
|
19
|
+
|
|
20
|
+
class_option :path, type: :string, default: nil,
|
|
21
|
+
desc: "Directory holding the bundle (default: <games_root>/<name>)"
|
|
22
|
+
class_option :at, type: :string, default: nil,
|
|
23
|
+
desc: "URL path to mount at (default: /<name>)"
|
|
24
|
+
|
|
25
|
+
def create_bundle_directory
|
|
26
|
+
empty_directory bundle_path
|
|
27
|
+
create_file File.join(bundle_path, ".keep"), "" unless File.exist?(File.join(destination_root, bundle_path, ".keep"))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def add_the_mount
|
|
31
|
+
routes = File.join(destination_root, "config/routes.rb")
|
|
32
|
+
unless File.exist?(routes)
|
|
33
|
+
say_status :skip, "config/routes.rb not found — add this yourself:\n#{mount_line}", :yellow
|
|
34
|
+
return
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
if File.read(routes).include?(%("#{url_path}"))
|
|
38
|
+
say_status :identical, "#{url_path} is already mounted in config/routes.rb", :blue
|
|
39
|
+
return
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
inject_into_file "config/routes.rb", comment + mount_line + "\n",
|
|
43
|
+
after: /Rails\.application\.routes\.draw do\n/
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def tell_them_what_is_missing
|
|
47
|
+
say_status :next, "put an HTML5 export into #{bundle_path}, e.g.:", :green
|
|
48
|
+
say " rake dragonruby:install[path/to/#{name}-html5.zip,#{name}]"
|
|
49
|
+
say_status :link, "then the game is at #{url_path}/ (the trailing slash matters in links)", :green
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def bundle_path
|
|
55
|
+
options[:path] || File.join(games_root, name)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def games_root
|
|
59
|
+
::Rails.application.config.dragonruby.games_root
|
|
60
|
+
rescue StandardError
|
|
61
|
+
"games"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def url_path
|
|
65
|
+
options[:at] || "/#{name}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def mount_line
|
|
69
|
+
%( mount DragonRuby::Rack::Game.new(Rails.root.join("#{bundle_path}")) => "#{url_path}"\n)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def comment
|
|
73
|
+
<<~RUBY
|
|
74
|
+
# #{name} — a DragonRuby HTML5 export, served cross-origin isolated so its
|
|
75
|
+
# WASM runtime can use SharedArrayBuffer. The bundle lives outside public/
|
|
76
|
+
# on purpose: Rails' static file server runs before the router and would
|
|
77
|
+
# serve it without those headers. Link to it as #{url_path}/
|
|
78
|
+
RUBY
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
require "dragonruby/rack/bundle"
|
|
2
|
+
|
|
3
|
+
namespace :dragonruby do
|
|
4
|
+
desc "Install a published HTML5 export: rake dragonruby:install[path/to/game-html5.zip,underwater]"
|
|
5
|
+
task :install, [:zip, :name] => :environment do |_task, args|
|
|
6
|
+
zip = args[:zip] or abort "usage: rake dragonruby:install[path/to/game-html5.zip,name]"
|
|
7
|
+
name = args[:name] or abort "usage: rake dragonruby:install[path/to/game-html5.zip,name]"
|
|
8
|
+
|
|
9
|
+
games_root = Rails.application.config.dragonruby.games_root
|
|
10
|
+
into = Rails.root.join(games_root, name)
|
|
11
|
+
|
|
12
|
+
path = DragonRuby::Rack::Bundle.install(zip, into, force: ENV["FORCE"] == "1")
|
|
13
|
+
size = Dir.glob(File.join(path, "**", "*")).sum { |f| File.file?(f) ? File.size(f) : 0 }
|
|
14
|
+
|
|
15
|
+
puts "Installed #{File.basename(zip)} into #{into} (#{(size / 1_048_576.0).round(1)} MB)"
|
|
16
|
+
puts "Mount it with: mount DragonRuby::Rack::Game.new(Rails.root.join(\"#{games_root}/#{name}\")) => \"/#{name}\""
|
|
17
|
+
rescue DragonRuby::Rack::Bundle::Error => e
|
|
18
|
+
abort "dragonruby:install — #{e.message}"
|
|
19
|
+
end
|
|
20
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: dragonruby-rack
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Robin Stammer
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rack
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.2'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '4'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '2.2'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '4'
|
|
32
|
+
description: |-
|
|
33
|
+
A mountable Rack endpoint that serves a DragonRuby GTK HTML5 export with the
|
|
34
|
+
cross-origin isolation headers its WASM runtime needs, and rebases the
|
|
35
|
+
bundle's relative asset links so it works under any mount path. Ships an
|
|
36
|
+
optional Rails generator and rake task; the Rack app itself needs no Rails.
|
|
37
|
+
email:
|
|
38
|
+
- robin.stammer@posteo.de
|
|
39
|
+
executables: []
|
|
40
|
+
extensions: []
|
|
41
|
+
extra_rdoc_files: []
|
|
42
|
+
files:
|
|
43
|
+
- CHANGELOG.md
|
|
44
|
+
- LICENSE.txt
|
|
45
|
+
- README.md
|
|
46
|
+
- lib/dragonruby-rack.rb
|
|
47
|
+
- lib/dragonruby/rack.rb
|
|
48
|
+
- lib/dragonruby/rack/bundle.rb
|
|
49
|
+
- lib/dragonruby/rack/game.rb
|
|
50
|
+
- lib/dragonruby/rack/railtie.rb
|
|
51
|
+
- lib/dragonruby/rack/version.rb
|
|
52
|
+
- lib/generators/dragonruby/mount/mount_generator.rb
|
|
53
|
+
- lib/tasks/dragonruby.rake
|
|
54
|
+
homepage: https://github.com/rstammer/dragonruby-rack
|
|
55
|
+
licenses:
|
|
56
|
+
- MIT
|
|
57
|
+
metadata:
|
|
58
|
+
homepage_uri: https://github.com/rstammer/dragonruby-rack
|
|
59
|
+
changelog_uri: https://github.com/rstammer/dragonruby-rack/blob/main/CHANGELOG.md
|
|
60
|
+
rubygems_mfa_required: 'true'
|
|
61
|
+
rdoc_options: []
|
|
62
|
+
require_paths:
|
|
63
|
+
- lib
|
|
64
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '3.1'
|
|
69
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - ">="
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: '0'
|
|
74
|
+
requirements: []
|
|
75
|
+
rubygems_version: 3.6.9
|
|
76
|
+
specification_version: 4
|
|
77
|
+
summary: Serve a DragonRuby GTK HTML5 export from a Rack or Rails app
|
|
78
|
+
test_files: []
|