lively 0.11.0 → 0.13.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/context/getting-started.md +77 -0
- data/context/index.yaml +16 -0
- data/context/worms-tutorial.md +842 -0
- data/lib/lively/application.rb +38 -0
- data/lib/lively/assets.rb +66 -14
- data/lib/lively/environment/application.rb +20 -5
- data/lib/lively/hello_world.rb +16 -0
- data/lib/lively/pages/index.rb +19 -1
- data/lib/lively/pages/index.xrb +2 -4
- data/lib/lively/version.rb +3 -2
- data/public/_components/@socketry/live/Live.js +42 -48
- data/public/_components/@socketry/live/package.json +4 -1
- data/public/_components/@socketry/live/readme.md +147 -31
- data/public/_components/@socketry/live-audio/Live/Audio/Controller.js +168 -0
- data/public/_components/@socketry/live-audio/Live/Audio/Library.js +748 -0
- data/public/_components/@socketry/live-audio/Live/Audio/Output.js +87 -0
- data/public/_components/@socketry/live-audio/Live/Audio/Sound.js +34 -0
- data/public/_components/@socketry/live-audio/Live/Audio/Visualizer.js +265 -0
- data/public/_components/@socketry/live-audio/Live/Audio.js +24 -0
- data/public/_components/@socketry/live-audio/package.json +35 -0
- data/public/_components/@socketry/live-audio/readme.md +250 -0
- data/public/application.js +4 -0
- data/readme.md +3 -7
- data.tar.gz.sig +0 -0
- metadata +15 -4
- metadata.gz.sig +0 -0
- data/public/_components/@socketry/live/test/Live.js +0 -357
data/lib/lively/application.rb
CHANGED
@@ -10,8 +10,18 @@ require "async/websocket/adapters/http"
|
|
10
10
|
require_relative "pages/index"
|
11
11
|
require_relative "hello_world"
|
12
12
|
|
13
|
+
# @namespace
|
13
14
|
module Lively
|
15
|
+
# Represents the main Lively application middleware.
|
16
|
+
#
|
17
|
+
# This class serves as the entry point for Lively applications, handling both
|
18
|
+
# standard HTTP requests for the initial page load and WebSocket connections
|
19
|
+
# for live updates. It integrates with the Live framework to provide real-time
|
20
|
+
# interactive web applications.
|
14
21
|
class Application < Protocol::HTTP::Middleware
|
22
|
+
# Create a new application class configured for a specific Live view tag.
|
23
|
+
# @parameter tag [Class] The Live view class to use as the application body.
|
24
|
+
# @returns [Class] A new application class configured for the specified tag.
|
15
25
|
def self.[](tag)
|
16
26
|
klass = Class.new(self)
|
17
27
|
|
@@ -26,36 +36,64 @@ module Lively
|
|
26
36
|
return klass
|
27
37
|
end
|
28
38
|
|
39
|
+
# Get the default resolver for this application.
|
40
|
+
# @returns [Live::Resolver] A resolver configured to allow HelloWorld components.
|
29
41
|
def self.resolver
|
30
42
|
Live::Resolver.allow(HelloWorld)
|
31
43
|
end
|
32
44
|
|
45
|
+
# Initialize a new Lively application.
|
46
|
+
# @parameter delegate [Protocol::HTTP::Middleware] The next middleware in the chain.
|
47
|
+
# @parameter resolver [Live::Resolver] The resolver for Live components.
|
33
48
|
def initialize(delegate, resolver: self.class.resolver)
|
34
49
|
super(delegate)
|
35
50
|
|
36
51
|
@resolver = resolver
|
37
52
|
end
|
38
53
|
|
54
|
+
# @attribute [Live::Resolver] The resolver for live components.
|
55
|
+
attr :resolver
|
56
|
+
|
57
|
+
# @attribute [Protocol::HTTP::Middleware] The delegate middleware for request handling.
|
58
|
+
attr :delegate
|
59
|
+
|
60
|
+
# Handle a WebSocket connection for live updates.
|
61
|
+
# @parameter connection [Async::WebSocket::Connection] The WebSocket connection.
|
39
62
|
def live(connection)
|
40
63
|
Live::Page.new(@resolver).run(connection)
|
41
64
|
end
|
42
65
|
|
66
|
+
# Get the title for this application.
|
67
|
+
# @returns [String] The class name of this application.
|
43
68
|
def title
|
44
69
|
self.class.name
|
45
70
|
end
|
46
71
|
|
72
|
+
# Create the body content for this application.
|
73
|
+
# @parameter **options [Hash] Additional options to pass to the body constructor.
|
74
|
+
# @returns [HelloWorld] A new HelloWorld instance.
|
47
75
|
def body(...)
|
48
76
|
HelloWorld.new(...)
|
49
77
|
end
|
50
78
|
|
79
|
+
# Create the index page for this application.
|
80
|
+
# @parameter **options [Hash] Additional options to pass to the index constructor.
|
81
|
+
# @returns [Pages::Index] A new index page instance.
|
51
82
|
def index(...)
|
52
83
|
Pages::Index.new(title: self.title, body: self.body(...))
|
53
84
|
end
|
54
85
|
|
86
|
+
# Handle a standard HTTP request.
|
87
|
+
# @parameter request [Protocol::HTTP::Request] The incoming HTTP request.
|
88
|
+
# @parameter **options [Hash] Additional options.
|
89
|
+
# @returns [Protocol::HTTP::Response] The HTTP response with the rendered page.
|
55
90
|
def handle(request, ...)
|
56
91
|
return Protocol::HTTP::Response[200, [], [self.index(...).call]]
|
57
92
|
end
|
58
93
|
|
94
|
+
# Process an incoming HTTP request.
|
95
|
+
# @parameter request [Protocol::HTTP::Request] The incoming HTTP request.
|
96
|
+
# @returns [Protocol::HTTP::Response] The appropriate response for the request.
|
59
97
|
def call(request)
|
60
98
|
if request.path == "/live"
|
61
99
|
return Async::WebSocket::Adapters::HTTP.open(request, &self.method(:live)) || Protocol::HTTP::Response[400]
|
data/lib/lively/assets.rb
CHANGED
@@ -5,8 +5,15 @@
|
|
5
5
|
|
6
6
|
require "protocol/http/middleware"
|
7
7
|
require "protocol/http/body/file"
|
8
|
+
require "console"
|
8
9
|
|
9
10
|
module Lively
|
11
|
+
# Represents an HTTP middleware for serving static assets.
|
12
|
+
#
|
13
|
+
# This middleware serves static files from a configured root directory with
|
14
|
+
# appropriate content type headers and caching controls. It supports a wide
|
15
|
+
# range of web file formats including HTML, CSS, JavaScript, images, fonts,
|
16
|
+
# audio, video, and WebAssembly files.
|
10
17
|
class Assets < Protocol::HTTP::Middleware
|
11
18
|
DEFAULT_CACHE_CONTROL = "no-store, no-cache, must-revalidate, max-age=0"
|
12
19
|
|
@@ -14,24 +21,53 @@ module Lively
|
|
14
21
|
".html" => "text/html",
|
15
22
|
".css" => "text/css",
|
16
23
|
".js" => "application/javascript",
|
24
|
+
".mjs" => "application/javascript",
|
25
|
+
".json" => "application/json",
|
17
26
|
".png" => "image/png",
|
27
|
+
".jpg" => "image/jpeg",
|
18
28
|
".jpeg" => "image/jpeg",
|
19
29
|
".gif" => "image/gif",
|
30
|
+
".webp" => "image/webp",
|
31
|
+
".svg" => "image/svg+xml",
|
32
|
+
".ico" => "image/x-icon",
|
20
33
|
".mp3" => "audio/mpeg",
|
21
34
|
".wav" => "audio/wav",
|
35
|
+
".ogg" => "audio/ogg",
|
36
|
+
".mp4" => "video/mp4",
|
37
|
+
".webm" => "video/webm",
|
38
|
+
".ttf" => "font/ttf",
|
39
|
+
".woff" => "font/woff",
|
40
|
+
".woff2" => "font/woff2",
|
41
|
+
".wasm" => "application/wasm",
|
22
42
|
}
|
23
43
|
|
24
44
|
PUBLIC_ROOT = File.expand_path("../../public", __dir__)
|
25
45
|
|
46
|
+
# Initialize a new assets middleware instance.
|
47
|
+
# @parameter delegate [Protocol::HTTP::Middleware] The next middleware in the chain.
|
48
|
+
# @parameter root [String] The root directory path for serving assets.
|
49
|
+
# @parameter content_types [Hash] Mapping of file extensions to MIME types.
|
50
|
+
# @parameter cache_control [String] Cache control header value for responses.
|
26
51
|
def initialize(delegate, root: PUBLIC_ROOT, content_types: DEFAULT_CONTENT_TYPES, cache_control: DEFAULT_CACHE_CONTROL)
|
27
52
|
super(delegate)
|
28
53
|
|
29
|
-
@root = root
|
54
|
+
@root = File.expand_path(root)
|
30
55
|
|
31
56
|
@content_types = content_types
|
32
57
|
@cache_control = cache_control
|
33
58
|
end
|
34
59
|
|
60
|
+
# @attribute [String] The absolute path to the root directory for serving assets.
|
61
|
+
attr :root
|
62
|
+
|
63
|
+
# @attribute [Hash] Mapping of file extensions to content types.
|
64
|
+
attr :content_types
|
65
|
+
|
66
|
+
# @attribute [String] Cache control header value for asset responses.
|
67
|
+
attr :cache_control
|
68
|
+
|
69
|
+
# Freeze this middleware instance and all its configuration.
|
70
|
+
# @returns [Assets] Returns self for method chaining.
|
35
71
|
def freeze
|
36
72
|
return self if frozen?
|
37
73
|
|
@@ -42,29 +78,45 @@ module Lively
|
|
42
78
|
super
|
43
79
|
end
|
44
80
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
81
|
+
# Generate an HTTP response for the given file path.
|
82
|
+
# @parameter path [String] The absolute file path to serve.
|
83
|
+
# @returns [Protocol::HTTP::Response] HTTP response with file content or error.
|
84
|
+
def response_for(path)
|
85
|
+
extension = File.extname(path)
|
50
86
|
|
51
|
-
|
87
|
+
if content_type = @content_types[extension]
|
88
|
+
headers = [
|
89
|
+
["content-type", content_type],
|
90
|
+
["cache-control", @cache_control],
|
91
|
+
]
|
92
|
+
|
93
|
+
return Protocol::HTTP::Response[200, headers, Protocol::HTTP::Body::File.open(path)]
|
94
|
+
else
|
95
|
+
Console.warn(self, "Unsupported media type!", path: path)
|
96
|
+
return Protocol::HTTP::Response[415, [["content-type", "text/plain"]], "Unsupported media type: #{extension.inspect}!"]
|
97
|
+
end
|
52
98
|
end
|
53
99
|
|
100
|
+
# Expand a relative path to an absolute path within the asset root.
|
101
|
+
# @parameter path [String] The relative path to expand.
|
102
|
+
# @returns [String | Nil] The absolute path if valid, `nil` if the file doesn't exist.
|
54
103
|
def expand_path(path)
|
55
|
-
File.realpath(
|
104
|
+
root = File.realpath(@root)
|
105
|
+
path = File.realpath(File.join(@root, path))
|
106
|
+
|
107
|
+
if path.start_with?(root) && File.file?(path)
|
108
|
+
return path
|
109
|
+
end
|
56
110
|
rescue Errno::ENOENT
|
57
111
|
nil
|
58
112
|
end
|
59
113
|
|
114
|
+
# Handle an incoming HTTP request.
|
115
|
+
# @parameter request [Protocol::HTTP::Request] The incoming HTTP request.
|
116
|
+
# @returns [Protocol::HTTP::Response] The HTTP response for the asset or delegates to next middleware.
|
60
117
|
def call(request)
|
61
118
|
if path = expand_path(request.path)
|
62
|
-
|
63
|
-
content_type = @content_types[extension]
|
64
|
-
|
65
|
-
if path.start_with?(@root) && File.exist?(path) && content_type
|
66
|
-
return response_for(path, content_type)
|
67
|
-
end
|
119
|
+
return response_for(path)
|
68
120
|
end
|
69
121
|
|
70
122
|
super
|
@@ -1,35 +1,50 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2021-
|
4
|
+
# Copyright, 2021-2025, by Samuel Williams.
|
5
5
|
|
6
6
|
require_relative "../application"
|
7
7
|
require_relative "../assets"
|
8
8
|
|
9
9
|
require "falcon/environment/server"
|
10
10
|
|
11
|
+
# @namespace
|
11
12
|
module Lively
|
13
|
+
# @namespace
|
12
14
|
module Environment
|
15
|
+
# Represents the environment configuration for a Lively application server.
|
16
|
+
#
|
17
|
+
# This module provides server configuration including URL binding, process count,
|
18
|
+
# application class resolution, and middleware stack setup. It integrates with
|
19
|
+
# Falcon's server environment to provide a complete hosting solution.
|
13
20
|
module Application
|
14
21
|
include Falcon::Environment::Server
|
15
22
|
|
16
|
-
#
|
17
|
-
#
|
18
|
-
|
23
|
+
# Get the server URL for this application.
|
24
|
+
# @returns [String] The base URL where the server will be accessible.
|
25
|
+
def url
|
26
|
+
"http://localhost:9292"
|
27
|
+
end
|
19
28
|
|
29
|
+
# Get the number of server processes to run.
|
30
|
+
# @returns [Integer] The number of worker processes.
|
20
31
|
def count
|
21
32
|
1
|
22
33
|
end
|
23
34
|
|
35
|
+
# Resolve the application class to use.
|
36
|
+
# @returns [Class] The application class, either user-defined or default.
|
24
37
|
def application
|
25
38
|
if Object.const_defined?(:Application)
|
26
|
-
|
39
|
+
Object.const_get(:Application)
|
27
40
|
else
|
28
41
|
Console.warn(self, "No Application class defined, using default.")
|
29
42
|
::Lively::Application
|
30
43
|
end
|
31
44
|
end
|
32
45
|
|
46
|
+
# Build the middleware stack for this application.
|
47
|
+
# @returns [Protocol::HTTP::Middleware] The complete middleware stack.
|
33
48
|
def middleware
|
34
49
|
::Protocol::HTTP::Middleware.build do |builder|
|
35
50
|
builder.use Lively::Assets, root: File.expand_path("public", self.root)
|
data/lib/lively/hello_world.rb
CHANGED
@@ -3,14 +3,27 @@
|
|
3
3
|
# Released under the MIT License.
|
4
4
|
# Copyright, 2024-2025, by Samuel Williams.
|
5
5
|
|
6
|
+
# @namespace
|
6
7
|
module Lively
|
8
|
+
# Represents a "Hello World" Live view component.
|
9
|
+
#
|
10
|
+
# This component displays a simple greeting message with a real-time clock
|
11
|
+
# that updates every second. It serves as a demonstration of Lively's
|
12
|
+
# live update capabilities and provides basic usage instructions.
|
7
13
|
class HelloWorld < Live::View
|
14
|
+
# Initialize a new HelloWorld view.
|
15
|
+
# @parameter **options [Hash] Additional options passed to the parent view.
|
8
16
|
def initialize(...)
|
9
17
|
super
|
10
18
|
|
11
19
|
@clock = nil
|
12
20
|
end
|
13
21
|
|
22
|
+
# @attribute [Async::Task | Nil] The background task that updates the view periodically.
|
23
|
+
attr :clock
|
24
|
+
|
25
|
+
# Bind this view to a page and start the update clock.
|
26
|
+
# @parameter page [Live::Page] The page this view is bound to.
|
14
27
|
def bind(page)
|
15
28
|
super
|
16
29
|
|
@@ -23,12 +36,15 @@ module Lively
|
|
23
36
|
end
|
24
37
|
end
|
25
38
|
|
39
|
+
# Close this view and stop the update clock.
|
26
40
|
def close
|
27
41
|
@clock&.stop
|
28
42
|
|
29
43
|
super
|
30
44
|
end
|
31
45
|
|
46
|
+
# Render this view as HTML.
|
47
|
+
# @parameter builder [Live::Builder] The HTML builder for constructing the view.
|
32
48
|
def render(builder)
|
33
49
|
builder.tag(:h1) do
|
34
50
|
builder.text("Hello, I'm Lively!")
|
data/lib/lively/pages/index.rb
CHANGED
@@ -5,10 +5,20 @@
|
|
5
5
|
|
6
6
|
require "xrb/template"
|
7
7
|
|
8
|
+
# @namespace
|
8
9
|
module Lively
|
10
|
+
# @namespace
|
9
11
|
module Pages
|
12
|
+
# Represents the main index page for a Lively application.
|
13
|
+
#
|
14
|
+
# This class renders the initial HTML page that users see when they visit
|
15
|
+
# a Lively application. It uses an XRB template to generate the page structure
|
16
|
+
# and embeds the Live view component for dynamic content.
|
10
17
|
class Index
|
11
|
-
|
18
|
+
# Initialize a new index page.
|
19
|
+
# @parameter title [String] The title of the page.
|
20
|
+
# @parameter body [Object] The body content of the page.
|
21
|
+
def initialize(title: "Lively", body: nil)
|
12
22
|
@title = title
|
13
23
|
@body = body
|
14
24
|
|
@@ -16,9 +26,17 @@ module Lively
|
|
16
26
|
@template = XRB::Template.load_file(path)
|
17
27
|
end
|
18
28
|
|
29
|
+
# @attribute [String] The title of the page.
|
19
30
|
attr :title
|
31
|
+
|
32
|
+
# @attribute [Object] The body content of the page.
|
20
33
|
attr :body
|
21
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.
|
22
40
|
def call
|
23
41
|
@template.to_string(self)
|
24
42
|
end
|
data/lib/lively/pages/index.xrb
CHANGED
@@ -14,15 +14,13 @@
|
|
14
14
|
{
|
15
15
|
"imports": {
|
16
16
|
"live": "/_components/@socketry/live/Live.js",
|
17
|
+
"live-audio": "/_components/@socketry/live-audio/Live/Audio.js",
|
17
18
|
"morphdom": "/_components/morphdom/morphdom-esm.js"
|
18
19
|
}
|
19
20
|
}
|
20
21
|
</script>
|
21
22
|
|
22
|
-
<script type="module">
|
23
|
-
import {Live} from 'live';
|
24
|
-
window.live = Live.start();
|
25
|
-
</script>
|
23
|
+
<script type="module" src="/application.js"></script>
|
26
24
|
</head>
|
27
25
|
|
28
26
|
<body>
|
data/lib/lively/version.rb
CHANGED
@@ -1,5 +1,33 @@
|
|
1
1
|
import morphdom from 'morphdom';
|
2
2
|
|
3
|
+
// ViewElement - Base class for Live.js custom elements
|
4
|
+
export class ViewElement extends HTMLElement {
|
5
|
+
static observedAttributes = [];
|
6
|
+
static connectedElements = new Set();
|
7
|
+
|
8
|
+
connectedCallback() {
|
9
|
+
if (!this.id) {
|
10
|
+
this.id = crypto.randomUUID();
|
11
|
+
}
|
12
|
+
|
13
|
+
ViewElement.connectedElements.add(this);
|
14
|
+
|
15
|
+
const window = this.ownerDocument.defaultView;
|
16
|
+
if (window && window.live) {
|
17
|
+
window.live.bind(this);
|
18
|
+
}
|
19
|
+
}
|
20
|
+
|
21
|
+
disconnectedCallback() {
|
22
|
+
ViewElement.connectedElements.delete(this);
|
23
|
+
|
24
|
+
const window = this.ownerDocument.defaultView;
|
25
|
+
if (window && window.live) {
|
26
|
+
window.live.unbind(this);
|
27
|
+
}
|
28
|
+
}
|
29
|
+
}
|
30
|
+
|
3
31
|
export class Live {
|
4
32
|
#window;
|
5
33
|
#document;
|
@@ -10,13 +38,20 @@ export class Live {
|
|
10
38
|
|
11
39
|
static start(options = {}) {
|
12
40
|
let window = options.window || globalThis;
|
41
|
+
if (window.live) throw new Error("Live.js is already started!");
|
42
|
+
|
13
43
|
let path = options.path || 'live'
|
14
44
|
let base = options.base || window.location.href;
|
15
45
|
|
16
46
|
let url = new URL(path, base);
|
17
47
|
url.protocol = url.protocol.replace('http', 'ws');
|
18
48
|
|
19
|
-
|
49
|
+
window.live = new this(window, url);
|
50
|
+
if (!window.customElements.get('live-view')) {
|
51
|
+
window.customElements.define('live-view', ViewElement);
|
52
|
+
}
|
53
|
+
|
54
|
+
return window.live;
|
20
55
|
}
|
21
56
|
|
22
57
|
constructor(window, url) {
|
@@ -34,43 +69,6 @@ export class Live {
|
|
34
69
|
this.#document.addEventListener("visibilitychange", () => this.#handleVisibilityChange());
|
35
70
|
|
36
71
|
this.#handleVisibilityChange();
|
37
|
-
|
38
|
-
const elementNodeType = this.#window.Node.ELEMENT_NODE;
|
39
|
-
|
40
|
-
// Create a MutationObserver to watch for removed nodes
|
41
|
-
this.observer = new this.#window.MutationObserver((mutationsList, observer) => {
|
42
|
-
for (let mutation of mutationsList) {
|
43
|
-
if (mutation.type === 'childList') {
|
44
|
-
for (let node of mutation.removedNodes) {
|
45
|
-
if (node.nodeType !== elementNodeType) continue;
|
46
|
-
|
47
|
-
if (node.classList?.contains('live')) {
|
48
|
-
this.#unbind(node);
|
49
|
-
}
|
50
|
-
|
51
|
-
// Unbind any child nodes:
|
52
|
-
for (let child of node.getElementsByClassName('live')) {
|
53
|
-
this.#unbind(child);
|
54
|
-
}
|
55
|
-
}
|
56
|
-
|
57
|
-
for (let node of mutation.addedNodes) {
|
58
|
-
if (node.nodeType !== elementNodeType) continue;
|
59
|
-
|
60
|
-
if (node.classList.contains('live')) {
|
61
|
-
this.#bind(node);
|
62
|
-
}
|
63
|
-
|
64
|
-
// Bind any child nodes:
|
65
|
-
for (let child of node.getElementsByClassName('live')) {
|
66
|
-
this.#bind(child);
|
67
|
-
}
|
68
|
-
}
|
69
|
-
}
|
70
|
-
}
|
71
|
-
});
|
72
|
-
|
73
|
-
this.observer.observe(this.#document.body, {childList: true, subtree: true});
|
74
72
|
}
|
75
73
|
|
76
74
|
// -- Connection Handling --
|
@@ -167,23 +165,19 @@ export class Live {
|
|
167
165
|
}
|
168
166
|
}
|
169
167
|
|
170
|
-
|
171
|
-
console.log("bind", element.id, element.dataset);
|
172
|
-
|
168
|
+
bind(element) {
|
173
169
|
this.#send(JSON.stringify(['bind', element.id, element.dataset]));
|
174
170
|
}
|
175
|
-
|
176
|
-
|
177
|
-
console.log("unbind", element.id, element.dataset);
|
178
|
-
|
171
|
+
|
172
|
+
unbind(element) {
|
179
173
|
if (this.#server) {
|
180
174
|
this.#send(JSON.stringify(['unbind', element.id]));
|
181
175
|
}
|
182
176
|
}
|
183
|
-
|
177
|
+
|
184
178
|
#attach() {
|
185
|
-
for (let
|
186
|
-
this
|
179
|
+
for (let element of ViewElement.connectedElements) {
|
180
|
+
this.bind(element);
|
187
181
|
}
|
188
182
|
}
|
189
183
|
|
@@ -1,9 +1,12 @@
|
|
1
1
|
{
|
2
2
|
"name": "@socketry/live",
|
3
3
|
"type": "module",
|
4
|
-
"version": "0.
|
4
|
+
"version": "0.16.0",
|
5
5
|
"description": "Live HTML tags for Ruby.",
|
6
6
|
"main": "Live.js",
|
7
|
+
"files": [
|
8
|
+
"Live.js"
|
9
|
+
],
|
7
10
|
"repository": {
|
8
11
|
"type": "git",
|
9
12
|
"url": "git+https://github.com/socketry/live-js.git"
|