lively 0.17.1 → 0.19.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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/bin/lively +3 -2
- data/context/getting-started.md +21 -13
- data/lib/lively/application.rb +22 -7
- data/lib/lively/assets.rb +1 -0
- data/lib/lively/environment/application.rb +48 -36
- data/lib/lively/environment/http.rb +34 -0
- data/lib/lively/environment/htty.rb +22 -0
- data/lib/lively/environment/middleware.rb +48 -0
- data/lib/lively/page.rb +103 -0
- data/lib/lively/page.xrb +30 -0
- data/lib/lively/pages/index.rb +22 -22
- data/lib/lively/resolver.rb +1 -1
- data/lib/lively/router.rb +138 -0
- data/lib/lively/version.rb +1 -1
- data/lib/lively.rb +2 -0
- data/readme.md +4 -7
- data/releases.md +4 -0
- data.tar.gz.sig +0 -0
- metadata +50 -3
- metadata.gz.sig +0 -0
- data/lib/lively/pages/index.xrb +0 -29
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d4f90097ca71e02dffa056e9251b96a63f0d45262ad5eddd0f073d2d02d42b6e
|
|
4
|
+
data.tar.gz: 214a19575d3f3fa624a3d71494b888feccebc18774fa1e38255ec4ceb90c84a6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 23cc87763d59471eaabdfee09f234f389c3576f7bb6d59df85f65e0205d7f6244710c20cfed77c3e0543513f4db581664e776062383c9a05491673ca8bfc88b8
|
|
7
|
+
data.tar.gz: 5b905206f71fcea3f15da3f6db3f0e8fcd9f1bd85f06278654d621affb4d72cf42828b828a1f6821602a98f8af5b8441fe701a4785a7626b4327f972dad8b03d
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/bin/lively
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require "async/service"
|
|
5
|
+
require "async/container/threaded"
|
|
5
6
|
require_relative "../lib/lively/environment/application"
|
|
6
7
|
|
|
7
8
|
ARGV.each do |path|
|
|
@@ -11,7 +12,7 @@ end
|
|
|
11
12
|
configuration = Async::Service::Configuration.build do
|
|
12
13
|
service "lively" do
|
|
13
14
|
include Lively::Environment::Application
|
|
14
|
-
end
|
|
15
|
+
end
|
|
15
16
|
end
|
|
16
17
|
|
|
17
|
-
Async::Service::Controller.run(configuration)
|
|
18
|
+
Async::Service::Controller.run(configuration, container_class: Async::Container::Threaded)
|
data/context/getting-started.md
CHANGED
|
@@ -99,7 +99,7 @@ Application = Lively::Application[GameView, game_state: GameState.new]
|
|
|
99
99
|
|
|
100
100
|
The `game_state:` keyword is passed to every `GameView` instance — whether created by the initial page load or by a WebSocket reconnection. This means all connected browsers share the same `GameState` object.
|
|
101
101
|
|
|
102
|
-
For more complex applications, subclass {ruby Lively::Application} and
|
|
102
|
+
For more complex applications, subclass {ruby Lively::Application} and add routes with `#configure_routes`:
|
|
103
103
|
|
|
104
104
|
```ruby
|
|
105
105
|
class Application < Lively::Application
|
|
@@ -116,26 +116,34 @@ class Application < Lively::Application
|
|
|
116
116
|
super
|
|
117
117
|
end
|
|
118
118
|
|
|
119
|
-
def
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
119
|
+
def configure_routes(router)
|
|
120
|
+
super
|
|
121
|
+
|
|
122
|
+
router.get("/") do
|
|
123
|
+
render(DisplayView.new(**state))
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
router.get("/control") do |_request, parameters|
|
|
127
|
+
render(ControlView.new(**state, mode: parameters["mode"]))
|
|
125
128
|
end
|
|
126
129
|
end
|
|
127
130
|
|
|
131
|
+
# Unmatched routes are passed here:
|
|
128
132
|
def handle(request)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
delegate.call(request)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
private
|
|
137
|
+
|
|
138
|
+
def render(body)
|
|
139
|
+
page = Lively::Pages::Index.new(title: "My App", body: body)
|
|
140
|
+
Protocol::HTTP::Response[200, [], [page.call]]
|
|
135
141
|
end
|
|
136
142
|
end
|
|
137
143
|
```
|
|
138
144
|
|
|
145
|
+
Routes match exact paths and may accept one or more HTTP methods. Query parameters are decoded using `protocol-url` and passed to the handler as its second argument. Routes without an explicit method accept every method.
|
|
146
|
+
|
|
139
147
|
## Live Reloading
|
|
140
148
|
|
|
141
149
|
To enable live reloading, add the `io-watch` gem to your `gems.rb` file:
|
data/lib/lively/application.rb
CHANGED
|
@@ -8,6 +8,7 @@ require "protocol/http/middleware"
|
|
|
8
8
|
require "async/websocket/adapters/http"
|
|
9
9
|
|
|
10
10
|
require_relative "resolver"
|
|
11
|
+
require_relative "router"
|
|
11
12
|
require_relative "pages/index"
|
|
12
13
|
require_relative "hello_world"
|
|
13
14
|
|
|
@@ -22,7 +23,7 @@ module Lively
|
|
|
22
23
|
#
|
|
23
24
|
# Use {.[]} to create a simple application class for a single view, optionally
|
|
24
25
|
# with shared state. For more complex applications, subclass and override
|
|
25
|
-
# {#allowed_views}, {#state}, and {#
|
|
26
|
+
# {#allowed_views}, {#state}, and {#configure_routes}.
|
|
26
27
|
class Application < Protocol::HTTP::Middleware
|
|
27
28
|
VIEWS = [HelloWorld].freeze
|
|
28
29
|
STATE = {}.freeze
|
|
@@ -98,22 +99,36 @@ module Lively
|
|
|
98
99
|
Pages::Index.new(title: self.title, body: self.body)
|
|
99
100
|
end
|
|
100
101
|
|
|
101
|
-
# Handle a standard HTTP request.
|
|
102
|
+
# Handle a standard HTTP request which did not match a configured route.
|
|
102
103
|
# @parameter request [Protocol::HTTP::Request] The incoming HTTP request.
|
|
103
104
|
# @returns [Protocol::HTTP::Response] The HTTP response with the rendered page.
|
|
104
105
|
def handle(request)
|
|
105
106
|
return Protocol::HTTP::Response[200, [], [self.index.call]]
|
|
106
107
|
end
|
|
107
108
|
|
|
109
|
+
# Add the standard application routes to the given router.
|
|
110
|
+
# Override this method and call `super` to add application-specific routes.
|
|
111
|
+
# @parameter router [Router] The router to configure.
|
|
112
|
+
def configure_routes(router)
|
|
113
|
+
router.route("/live") do |request|
|
|
114
|
+
Async::WebSocket::Adapters::HTTP.open(request, &self.method(:live)) || Protocol::HTTP::Response[400]
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# The router for this application. Unmatched requests are handled separately
|
|
119
|
+
# by {#handle}.
|
|
120
|
+
# @returns [Router] The configured router.
|
|
121
|
+
def router
|
|
122
|
+
@router ||= Router.new.tap do |router|
|
|
123
|
+
configure_routes(router)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
108
127
|
# Process an incoming HTTP request.
|
|
109
128
|
# @parameter request [Protocol::HTTP::Request] The incoming HTTP request.
|
|
110
129
|
# @returns [Protocol::HTTP::Response] The appropriate response for the request.
|
|
111
130
|
def call(request)
|
|
112
|
-
|
|
113
|
-
return Async::WebSocket::Adapters::HTTP.open(request, &self.method(:live)) || Protocol::HTTP::Response[400]
|
|
114
|
-
else
|
|
115
|
-
return handle(request)
|
|
116
|
-
end
|
|
131
|
+
return router.call(request) || handle(request)
|
|
117
132
|
end
|
|
118
133
|
end
|
|
119
134
|
end
|
data/lib/lively/assets.rb
CHANGED
|
@@ -1,56 +1,68 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2021-
|
|
4
|
+
# Copyright, 2021-2026, by Samuel Williams.
|
|
5
5
|
|
|
6
|
-
require_relative "
|
|
7
|
-
require_relative "
|
|
8
|
-
|
|
9
|
-
require "falcon/environment/server"
|
|
6
|
+
require_relative "middleware"
|
|
7
|
+
require_relative "http"
|
|
8
|
+
require_relative "htty"
|
|
10
9
|
|
|
11
10
|
# @namespace
|
|
12
11
|
module Lively
|
|
13
12
|
# @namespace
|
|
14
13
|
module Environment
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
# This
|
|
18
|
-
#
|
|
19
|
-
#
|
|
14
|
+
# Multiplexing environment for Lively applications.
|
|
15
|
+
#
|
|
16
|
+
# Declares the transport selection as explicit, overridable evaluator keys and uses `make_service` to compose the appropriate child environment at service startup time. This keeps transport selection in the service layer rather than in module inclusion hooks.
|
|
17
|
+
#
|
|
18
|
+
# The `htty` key controls which transport is used. Override it in a service block to force a specific transport regardless of the environment variable:
|
|
19
|
+
#
|
|
20
|
+
# ~~~ ruby
|
|
21
|
+
# service "myapp" do
|
|
22
|
+
# include Lively::Environment::Application
|
|
23
|
+
# def htty = false # always use HTTP
|
|
24
|
+
# end
|
|
25
|
+
# ~~~
|
|
20
26
|
module Application
|
|
21
|
-
include
|
|
27
|
+
include Lively::Environment::Middleware
|
|
28
|
+
# Note: does not include Falcon::Environment::Server directly. Falcon is
|
|
29
|
+
# brought in exclusively via http_environment so that the combined
|
|
30
|
+
# evaluator's service_class resolves correctly without shadowing.
|
|
31
|
+
|
|
32
|
+
# Whether to use HTTY transport. Reads ENV["HTTY"] by default.
|
|
33
|
+
# @returns [Boolean]
|
|
34
|
+
def htty
|
|
35
|
+
ENV["HTTY"] == "1"
|
|
36
|
+
end
|
|
22
37
|
|
|
23
|
-
#
|
|
24
|
-
# @returns [
|
|
25
|
-
def
|
|
26
|
-
|
|
38
|
+
# The environment module to use for HTTY transport.
|
|
39
|
+
# @returns [Module]
|
|
40
|
+
def htty_environment
|
|
41
|
+
Lively::Environment::HTTY
|
|
27
42
|
end
|
|
28
43
|
|
|
29
|
-
#
|
|
30
|
-
# @returns [
|
|
31
|
-
def
|
|
32
|
-
|
|
44
|
+
# The environment module to use for HTTP transport.
|
|
45
|
+
# @returns [Module]
|
|
46
|
+
def http_environment
|
|
47
|
+
Lively::Environment::HTTP
|
|
33
48
|
end
|
|
34
49
|
|
|
35
|
-
#
|
|
36
|
-
# @returns [
|
|
37
|
-
def
|
|
38
|
-
|
|
39
|
-
Object.const_get(:Application)
|
|
40
|
-
else
|
|
41
|
-
Console.warn(self, "No Application class defined, using default.")
|
|
42
|
-
::Lively::Application
|
|
43
|
-
end
|
|
50
|
+
# The environment module for the selected transport.
|
|
51
|
+
# @returns [Module]
|
|
52
|
+
def transport_environment
|
|
53
|
+
htty ? htty_environment : http_environment
|
|
44
54
|
end
|
|
45
55
|
|
|
46
|
-
# Build the
|
|
47
|
-
#
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
# Build the service by composing the transport environment on top of this one.
|
|
57
|
+
# Called by Async::Service::Generic.wrap — self is the evaluator at call time.
|
|
58
|
+
# @parameter environment [Async::Service::Environment]
|
|
59
|
+
# @returns [Async::Service::Generic]
|
|
60
|
+
def make_service(environment)
|
|
61
|
+
combined = environment.with(transport_environment)
|
|
62
|
+
combined_evaluator = combined.evaluator
|
|
63
|
+
|
|
64
|
+
# Call `service_class.new` directly rather than `Async::Service::Generic.wrap` — the combined evaluator still has `Application` (and therefore `make_service`) in its ancestor chain, so `wrap` would recurse back into this method.
|
|
65
|
+
return combined_evaluator.service_class.new(combined, combined_evaluator)
|
|
54
66
|
end
|
|
55
67
|
end
|
|
56
68
|
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2021-2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require_relative "middleware"
|
|
7
|
+
require "falcon/environment/server"
|
|
8
|
+
|
|
9
|
+
# @namespace
|
|
10
|
+
module Lively
|
|
11
|
+
# @namespace
|
|
12
|
+
module Environment
|
|
13
|
+
# Falcon (TCP/HTTP) environment for Lively applications.
|
|
14
|
+
#
|
|
15
|
+
# Combines {Falcon::Environment::Server} for HTTP transport with
|
|
16
|
+
# {Lively::Environment::Middleware} for application and asset serving.
|
|
17
|
+
module HTTP
|
|
18
|
+
include Falcon::Environment::Server
|
|
19
|
+
include Lively::Environment::Middleware
|
|
20
|
+
|
|
21
|
+
# The URL this server binds to.
|
|
22
|
+
# @returns [String]
|
|
23
|
+
def url
|
|
24
|
+
ENV.fetch("LIVELY_URL", "http://localhost:9292")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# The number of worker processes/threads to run.
|
|
28
|
+
# @returns [Integer]
|
|
29
|
+
def count
|
|
30
|
+
1
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require_relative "middleware"
|
|
7
|
+
require "async/htty/environment/server"
|
|
8
|
+
|
|
9
|
+
# @namespace
|
|
10
|
+
module Lively
|
|
11
|
+
# @namespace
|
|
12
|
+
module Environment
|
|
13
|
+
# HTTY (terminal side-channel) environment for Lively applications.
|
|
14
|
+
#
|
|
15
|
+
# Combines {Async::HTTY::Environment} for HTTY transport with
|
|
16
|
+
# {Lively::Environment::Middleware} for application and asset serving.
|
|
17
|
+
module HTTY
|
|
18
|
+
include Async::HTTY::Environment::Server
|
|
19
|
+
include Lively::Environment::Middleware
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2021-2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require_relative "../application"
|
|
7
|
+
require_relative "../assets"
|
|
8
|
+
|
|
9
|
+
require "protocol/http/middleware/builder"
|
|
10
|
+
|
|
11
|
+
# @namespace
|
|
12
|
+
module Lively
|
|
13
|
+
# @namespace
|
|
14
|
+
module Environment
|
|
15
|
+
# Shared middleware configuration for Lively application environments.
|
|
16
|
+
#
|
|
17
|
+
# Provides the application class resolver, asset middleware, and the
|
|
18
|
+
# Lively middleware stack. Included by both {HTTP} and {HTTY} environments.
|
|
19
|
+
module Middleware
|
|
20
|
+
# Get the root directory for this application.
|
|
21
|
+
# @returns [String] The current working directory.
|
|
22
|
+
def root
|
|
23
|
+
Dir.pwd
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Resolve the application class to use.
|
|
27
|
+
# @returns [Class] The application class, either user-defined or default.
|
|
28
|
+
def application
|
|
29
|
+
if Object.const_defined?(:Application)
|
|
30
|
+
Object.const_get(:Application)
|
|
31
|
+
else
|
|
32
|
+
Console.warn(self, "No Application class defined, using default.")
|
|
33
|
+
::Lively::Application
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Build the middleware stack for this application.
|
|
38
|
+
# @returns [Protocol::HTTP::Middleware] The complete middleware stack.
|
|
39
|
+
def middleware
|
|
40
|
+
::Protocol::HTTP::Middleware.build do |builder|
|
|
41
|
+
builder.use Lively::Assets, root: File.expand_path("public", self.root)
|
|
42
|
+
builder.use Lively::Assets, root: File.expand_path("../../../public", __dir__)
|
|
43
|
+
builder.use self.application
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
data/lib/lively/page.rb
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "json"
|
|
7
|
+
require "xrb/markup"
|
|
8
|
+
require "xrb/tag"
|
|
9
|
+
require "xrb/template"
|
|
10
|
+
|
|
11
|
+
module Lively
|
|
12
|
+
# Represents a complete HTML document.
|
|
13
|
+
#
|
|
14
|
+
# A page combines application content with the stylesheets, import map,
|
|
15
|
+
# JavaScript modules, and body attributes required to present it. Applications
|
|
16
|
+
# can use this class directly or subclass it to provide shared defaults.
|
|
17
|
+
class Page
|
|
18
|
+
TEMPLATE = XRB::Template.load_file(File.expand_path("page.xrb", __dir__))
|
|
19
|
+
|
|
20
|
+
# Initialize a new page.
|
|
21
|
+
# @parameter title [String] The document title.
|
|
22
|
+
# @parameter body [Object | Nil] The document body. The result of `to_html` is interpolated into the template.
|
|
23
|
+
# @parameter icon [String | Nil] The favicon URL.
|
|
24
|
+
# @parameter stylesheets [Array(String | Hash)] Stylesheets in document order. Hash entries specify link attributes.
|
|
25
|
+
# @parameter imports [Hash] JavaScript import map entries.
|
|
26
|
+
# @parameter modules [Array(String)] JavaScript module URLs in document order.
|
|
27
|
+
# @parameter body_attributes [Hash] Attributes applied to the body element.
|
|
28
|
+
def initialize(title: "Lively", body: nil, icon: nil, stylesheets: [], imports: {}, modules: [], body_attributes: {})
|
|
29
|
+
@title = title
|
|
30
|
+
@body = body
|
|
31
|
+
@icon = icon
|
|
32
|
+
@stylesheets = stylesheets
|
|
33
|
+
@imports = imports
|
|
34
|
+
@modules = modules
|
|
35
|
+
@body_attributes = body_attributes
|
|
36
|
+
@template = TEMPLATE
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @attribute [String] The document title.
|
|
40
|
+
attr :title
|
|
41
|
+
|
|
42
|
+
# @attribute [Object | Nil] The document body.
|
|
43
|
+
attr :body
|
|
44
|
+
|
|
45
|
+
# @attribute [String | Nil] The favicon URL.
|
|
46
|
+
attr :icon
|
|
47
|
+
|
|
48
|
+
# @attribute [Array(String | Hash)] Stylesheets in document order.
|
|
49
|
+
attr :stylesheets
|
|
50
|
+
|
|
51
|
+
# @attribute [Hash] JavaScript import map entries.
|
|
52
|
+
attr :imports
|
|
53
|
+
|
|
54
|
+
# @attribute [Array(String)] JavaScript module URLs in document order.
|
|
55
|
+
attr :modules
|
|
56
|
+
|
|
57
|
+
# @attribute [Hash] Attributes applied to the body element.
|
|
58
|
+
attr :body_attributes
|
|
59
|
+
|
|
60
|
+
# @attribute [XRB::Template] The document template.
|
|
61
|
+
attr :template
|
|
62
|
+
|
|
63
|
+
# The opening body tag including configured attributes.
|
|
64
|
+
# @returns [XRB::Tag]
|
|
65
|
+
def body_tag
|
|
66
|
+
XRB::Tag.opened("body", @body_attributes)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# A stylesheet link tag for the given URL or attributes.
|
|
70
|
+
# @parameter stylesheet [String | Hash] The stylesheet URL or link attributes.
|
|
71
|
+
# @returns [XRB::Tag]
|
|
72
|
+
def stylesheet_tag(stylesheet)
|
|
73
|
+
attributes = if stylesheet.respond_to?(:to_hash)
|
|
74
|
+
stylesheet.to_hash
|
|
75
|
+
else
|
|
76
|
+
{href: stylesheet}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
XRB::Tag.closed("link", {rel: "stylesheet", type: "text/css"}.merge(attributes))
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# The rendered body content.
|
|
83
|
+
# @returns [Object]
|
|
84
|
+
def body_content
|
|
85
|
+
@body&.to_html || "No body specified!"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# The serialized JavaScript import map.
|
|
89
|
+
# @returns [XRB::MarkupString]
|
|
90
|
+
def import_map
|
|
91
|
+
json = JSON.pretty_generate(imports: @imports)
|
|
92
|
+
json = json.gsub("<", "\\u003c").gsub(">", "\\u003e").gsub("&", "\\u0026")
|
|
93
|
+
|
|
94
|
+
XRB::MarkupString.raw(json)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Render this page to a string.
|
|
98
|
+
# @returns [String]
|
|
99
|
+
def call
|
|
100
|
+
@template.to_string(self)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
data/lib/lively/page.xrb
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>#{self.title}</title>
|
|
5
|
+
|
|
6
|
+
<meta charset="UTF-8" />
|
|
7
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
8
|
+
|
|
9
|
+
<?r if self.icon ?>
|
|
10
|
+
<link rel="icon" type="image/png" href="#{self.icon}" />
|
|
11
|
+
<?r end ?>
|
|
12
|
+
<?r self.stylesheets.each do |stylesheet| ?>
|
|
13
|
+
#{self.stylesheet_tag(stylesheet)}
|
|
14
|
+
<?r end ?>
|
|
15
|
+
|
|
16
|
+
<?r unless self.imports.empty? ?>
|
|
17
|
+
<script type="importmap">
|
|
18
|
+
#{self.import_map}
|
|
19
|
+
</script>
|
|
20
|
+
<?r end ?>
|
|
21
|
+
|
|
22
|
+
<?r self.modules.each do |source| ?>
|
|
23
|
+
<script type="module" src="#{source}"></script>
|
|
24
|
+
<?r end ?>
|
|
25
|
+
</head>
|
|
26
|
+
|
|
27
|
+
#{self.body_tag}
|
|
28
|
+
#{self.body_content}
|
|
29
|
+
</body>
|
|
30
|
+
</html>
|
data/lib/lively/pages/index.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
4
|
# Copyright, 2021-2026, by Samuel Williams.
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
require_relative "../page"
|
|
7
7
|
|
|
8
8
|
# @namespace
|
|
9
9
|
module Lively
|
|
@@ -14,31 +14,31 @@ module Lively
|
|
|
14
14
|
# This class renders the initial HTML page that users see when they visit
|
|
15
15
|
# a Lively application. It uses an XRB template to generate the page structure
|
|
16
16
|
# and embeds the Live view component for dynamic content.
|
|
17
|
-
class Index
|
|
17
|
+
class Index < Page
|
|
18
|
+
ICON = "/_static/icon.png"
|
|
19
|
+
STYLESHEETS = [
|
|
20
|
+
{href: "/_static/site.css", media: "screen"}.freeze,
|
|
21
|
+
{href: "/_static/index.css", media: "screen"}.freeze,
|
|
22
|
+
].freeze
|
|
23
|
+
IMPORTS = {
|
|
24
|
+
"live" => "/_components/@socketry/live/Live.js",
|
|
25
|
+
"live-audio" => "/_components/@socketry/live-audio/Live/Audio.js",
|
|
26
|
+
"morphdom" => "/_components/morphdom/morphdom-esm.js",
|
|
27
|
+
}.freeze
|
|
28
|
+
MODULES = ["/application.js"].freeze
|
|
29
|
+
|
|
18
30
|
# Initialize a new index page.
|
|
19
31
|
# @parameter title [String] The title of the page.
|
|
20
32
|
# @parameter body [Object] The body content of the page.
|
|
21
33
|
def initialize(title: "Lively", body: nil)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
attr :title
|
|
31
|
-
|
|
32
|
-
# @attribute [Object] The body content of the page.
|
|
33
|
-
attr :body
|
|
34
|
-
|
|
35
|
-
# @attribute [XRB::Template] The XRB template for rendering the page.
|
|
36
|
-
attr :template
|
|
37
|
-
|
|
38
|
-
# Render this page to a string.
|
|
39
|
-
# @returns [String] The rendered HTML for this page.
|
|
40
|
-
def call
|
|
41
|
-
@template.to_string(self)
|
|
34
|
+
super(
|
|
35
|
+
title: title,
|
|
36
|
+
body: body,
|
|
37
|
+
icon: ICON,
|
|
38
|
+
stylesheets: STYLESHEETS,
|
|
39
|
+
imports: IMPORTS,
|
|
40
|
+
modules: MODULES,
|
|
41
|
+
)
|
|
42
42
|
end
|
|
43
43
|
end
|
|
44
44
|
end
|
data/lib/lively/resolver.rb
CHANGED
|
@@ -13,7 +13,7 @@ module Lively
|
|
|
13
13
|
class Resolver < Live::Resolver
|
|
14
14
|
# Initialize a new resolver with shared state.
|
|
15
15
|
# @parameter state [Hash] Key-value pairs to pass to view constructors as keyword arguments.
|
|
16
|
-
def initialize(state =
|
|
16
|
+
def initialize(state = {})
|
|
17
17
|
super()
|
|
18
18
|
@state = state
|
|
19
19
|
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "protocol/http"
|
|
7
|
+
require "protocol/url"
|
|
8
|
+
|
|
9
|
+
module Lively
|
|
10
|
+
# Dispatches HTTP requests to handlers using exact path and method matches.
|
|
11
|
+
#
|
|
12
|
+
# Request targets are parsed with {Protocol::URL::Reference}. Handlers receive
|
|
13
|
+
# the original request and the decoded query parameters.
|
|
14
|
+
class Router
|
|
15
|
+
EMPTY_PARAMETERS = {}.freeze
|
|
16
|
+
ANY_METHOD = nil
|
|
17
|
+
private_constant :EMPTY_PARAMETERS, :ANY_METHOD
|
|
18
|
+
|
|
19
|
+
# Initialize a router.
|
|
20
|
+
# @yields {|router| ...} The router to configure.
|
|
21
|
+
def initialize
|
|
22
|
+
@routes = {}
|
|
23
|
+
|
|
24
|
+
yield self if block_given?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Add a route.
|
|
28
|
+
#
|
|
29
|
+
# When `methods` is omitted, the handler accepts every HTTP method. Otherwise,
|
|
30
|
+
# it may be a single method or an array of methods.
|
|
31
|
+
#
|
|
32
|
+
# @parameter path [String] The absolute path to match.
|
|
33
|
+
# @parameter methods [String | Symbol | Array(String | Symbol) | Nil] The accepted HTTP methods.
|
|
34
|
+
# @yields {|request, parameters| ...} The route handler.
|
|
35
|
+
# @parameter request [Protocol::HTTP::Request] The original request.
|
|
36
|
+
# @parameter parameters [Hash] The decoded query parameters.
|
|
37
|
+
# @returns [Router] The router.
|
|
38
|
+
def route(path, methods: nil, &handler)
|
|
39
|
+
raise ArgumentError, "A route handler is required!" unless handler
|
|
40
|
+
|
|
41
|
+
path = route_path(path)
|
|
42
|
+
methods = route_methods(methods)
|
|
43
|
+
handlers = (@routes[path] ||= {})
|
|
44
|
+
|
|
45
|
+
methods.each do |method|
|
|
46
|
+
if handlers.key?(method)
|
|
47
|
+
raise ArgumentError, "Route already defined for #{method || "any method"} #{path}!"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
handlers[method] = handler
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
return self
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
Protocol::HTTP::Methods.each do |name, method|
|
|
57
|
+
# Add a route for this HTTP method.
|
|
58
|
+
# @parameter path [String] The absolute path to match.
|
|
59
|
+
# @yields {|request, parameters| ...} The route handler.
|
|
60
|
+
# @returns [Router] The router.
|
|
61
|
+
define_method(name) do |path, &handler|
|
|
62
|
+
route(path, methods: method, &handler)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Dispatch a request to a matching route.
|
|
67
|
+
# @parameter request [Protocol::HTTP::Request] The request to dispatch.
|
|
68
|
+
# @returns [Protocol::HTTP::Response | Nil] The handler or error response, or `nil` when no path matches.
|
|
69
|
+
def call(request)
|
|
70
|
+
reference = parse_reference(request.path)
|
|
71
|
+
return Protocol::HTTP::Response[400] unless reference
|
|
72
|
+
|
|
73
|
+
unless handlers = @routes[reference.path]
|
|
74
|
+
return nil
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
unless handler = handlers[request.method] || handlers[ANY_METHOD]
|
|
78
|
+
allowed_methods = handlers.keys.compact.sort.join(", ")
|
|
79
|
+
return Protocol::HTTP::Response[405, [["allow", allowed_methods]]]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
parameters = parse_query(reference)
|
|
83
|
+
return Protocol::HTTP::Response[400] unless parameters
|
|
84
|
+
|
|
85
|
+
return handler.call(request, parameters)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def route_path(path)
|
|
91
|
+
url = Protocol::URL[path]
|
|
92
|
+
|
|
93
|
+
unless url && !url.is_a?(Protocol::URL::Absolute)
|
|
94
|
+
raise ArgumentError, "Route must be an absolute path: #{path.inspect}!"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
reference = Protocol::URL::Reference[url]
|
|
98
|
+
|
|
99
|
+
unless reference.path.absolute?
|
|
100
|
+
raise ArgumentError, "Route must be an absolute path: #{path.inspect}!"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
if reference.query? || reference.fragment?
|
|
104
|
+
raise ArgumentError, "Route path cannot include a query or fragment: #{path.inspect}!"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
return reference.path.freeze
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def route_methods(methods)
|
|
111
|
+
return [ANY_METHOD] unless methods
|
|
112
|
+
|
|
113
|
+
methods = Array(methods)
|
|
114
|
+
raise ArgumentError, "At least one HTTP method is required!" if methods.empty?
|
|
115
|
+
|
|
116
|
+
return methods.map do |method|
|
|
117
|
+
raise ArgumentError, "HTTP method cannot be nil!" unless method
|
|
118
|
+
|
|
119
|
+
method.to_s.upcase
|
|
120
|
+
end.uniq
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def parse_reference(path)
|
|
124
|
+
reference = Protocol::URL::Reference[path]
|
|
125
|
+
return if reference&.fragment?
|
|
126
|
+
|
|
127
|
+
return reference
|
|
128
|
+
rescue ArgumentError
|
|
129
|
+
nil
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def parse_query(reference)
|
|
133
|
+
reference.parse_query! || EMPTY_PARAMETERS
|
|
134
|
+
rescue ArgumentError
|
|
135
|
+
nil
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
data/lib/lively/version.rb
CHANGED
data/lib/lively.rb
CHANGED
data/readme.md
CHANGED
|
@@ -20,6 +20,10 @@ Please see the [project documentation](https://socketry.github.io/lively/) for m
|
|
|
20
20
|
|
|
21
21
|
Please see the [project releases](https://socketry.github.io/lively/releases/index) for all releases.
|
|
22
22
|
|
|
23
|
+
### v0.18.0
|
|
24
|
+
|
|
25
|
+
- Add support for HTTY.
|
|
26
|
+
|
|
23
27
|
### v0.17.0
|
|
24
28
|
|
|
25
29
|
- Expose shared application state via `Application[..., controller: Controller.new]`.
|
|
@@ -57,13 +61,6 @@ Please see the [project releases](https://socketry.github.io/lively/releases/ind
|
|
|
57
61
|
|
|
58
62
|
- Tidied up gem dependencies.
|
|
59
63
|
|
|
60
|
-
### v0.13.0
|
|
61
|
-
|
|
62
|
-
- Added `live-audio` support for background and positional audio in applications.
|
|
63
|
-
- Added game audio example demonstrating audio playback.
|
|
64
|
-
- Fixed serving non-existent asset paths gracefully.
|
|
65
|
-
- Achieved 100% test and documentation coverage.
|
|
66
|
-
|
|
67
64
|
## See Also
|
|
68
65
|
|
|
69
66
|
- [live](https://github.com/socketry/live) — Provides client-server communication using websockets.
|
data/releases.md
CHANGED
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lively
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.19.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Samuel Williams
|
|
@@ -52,6 +52,34 @@ dependencies:
|
|
|
52
52
|
- - ">="
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
54
|
version: '0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: async-htty
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0'
|
|
62
|
+
type: :runtime
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '0'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: async-service
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0.23'
|
|
76
|
+
type: :runtime
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0.23'
|
|
55
83
|
- !ruby/object:Gem::Dependency
|
|
56
84
|
name: falcon
|
|
57
85
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -94,6 +122,20 @@ dependencies:
|
|
|
94
122
|
- - "~>"
|
|
95
123
|
- !ruby/object:Gem::Version
|
|
96
124
|
version: '0.18'
|
|
125
|
+
- !ruby/object:Gem::Dependency
|
|
126
|
+
name: protocol-url
|
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
|
128
|
+
requirements:
|
|
129
|
+
- - "~>"
|
|
130
|
+
- !ruby/object:Gem::Version
|
|
131
|
+
version: '0.18'
|
|
132
|
+
type: :runtime
|
|
133
|
+
prerelease: false
|
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
135
|
+
requirements:
|
|
136
|
+
- - "~>"
|
|
137
|
+
- !ruby/object:Gem::Version
|
|
138
|
+
version: '0.18'
|
|
97
139
|
- !ruby/object:Gem::Dependency
|
|
98
140
|
name: xrb
|
|
99
141
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -123,10 +165,15 @@ files:
|
|
|
123
165
|
- lib/lively/application.rb
|
|
124
166
|
- lib/lively/assets.rb
|
|
125
167
|
- lib/lively/environment/application.rb
|
|
168
|
+
- lib/lively/environment/http.rb
|
|
169
|
+
- lib/lively/environment/htty.rb
|
|
170
|
+
- lib/lively/environment/middleware.rb
|
|
126
171
|
- lib/lively/hello_world.rb
|
|
172
|
+
- lib/lively/page.rb
|
|
173
|
+
- lib/lively/page.xrb
|
|
127
174
|
- lib/lively/pages/index.rb
|
|
128
|
-
- lib/lively/pages/index.xrb
|
|
129
175
|
- lib/lively/resolver.rb
|
|
176
|
+
- lib/lively/router.rb
|
|
130
177
|
- lib/lively/version.rb
|
|
131
178
|
- license.md
|
|
132
179
|
- public/_components/@socketry/live-audio/Live/Audio.js
|
|
@@ -172,7 +219,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
172
219
|
- !ruby/object:Gem::Version
|
|
173
220
|
version: '0'
|
|
174
221
|
requirements: []
|
|
175
|
-
rubygems_version: 4.0.
|
|
222
|
+
rubygems_version: 4.0.10
|
|
176
223
|
specification_version: 4
|
|
177
224
|
summary: A simple client-server SPA framework.
|
|
178
225
|
test_files: []
|
metadata.gz.sig
CHANGED
|
Binary file
|
data/lib/lively/pages/index.xrb
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html>
|
|
3
|
-
<head>
|
|
4
|
-
<title>#{self.title}</title>
|
|
5
|
-
|
|
6
|
-
<meta charset="UTF-8" />
|
|
7
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
8
|
-
|
|
9
|
-
<link rel="icon" type="image/png" href="/_static/icon.png" />
|
|
10
|
-
<link rel="stylesheet" href="/_static/site.css" type="text/css" media="screen" />
|
|
11
|
-
<link rel="stylesheet" href="/_static/index.css" type="text/css" media="screen" />
|
|
12
|
-
|
|
13
|
-
<script type="importmap">
|
|
14
|
-
{
|
|
15
|
-
"imports": {
|
|
16
|
-
"live": "/_components/@socketry/live/Live.js",
|
|
17
|
-
"live-audio": "/_components/@socketry/live-audio/Live/Audio.js",
|
|
18
|
-
"morphdom": "/_components/morphdom/morphdom-esm.js"
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
</script>
|
|
22
|
-
|
|
23
|
-
<script type="module" src="/application.js"></script>
|
|
24
|
-
</head>
|
|
25
|
-
|
|
26
|
-
<body>
|
|
27
|
-
#{self.body&.to_html || "No body specified!"}
|
|
28
|
-
</body>
|
|
29
|
-
</html>
|