mata 0.5.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/README.md +109 -0
- data/lib/mata/agent.rb +35 -0
- data/lib/mata/broadcaster.rb +97 -0
- data/lib/mata/client.js +65 -0
- data/lib/mata/idiomorph.min.js +1 -0
- data/lib/mata/version.rb +3 -0
- data/lib/mata/watch_tower.rb +55 -0
- data/lib/mata.rb +40 -0
- metadata +78 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 307e9528d8950443b07d7ebcf1f036d400fa2198f4f97e79959826011d6b543c
|
|
4
|
+
data.tar.gz: c3f3999e6d1c5dad7d224b34595ece06cb42c832cb25aa34b948cf2723083d2d
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 42d6e90cd1c19cece9a011056cf04c095673f9552dd0944f3c77bd4f807ac8575505cf889ed0d6d9fec7aa65bec23c7437d0d2d2ecab7206bab0fe2a89e10dbc
|
|
7
|
+
data.tar.gz: 29163a7985e9a8e863e374cfe4525902f7fe5861bb457b724444464c22b6159058ad8262ad62f7d33c509f14bf11887c29b4398ea86144d70b07e3d63cfe8a8f
|
data/README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Mata
|
|
2
|
+
|
|
3
|
+
Hot module reloading for Rack applications using Server-Sent Events and DOM morphing.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
**Sponsored By [Rails Designer](https://railsdesigner.com/)**
|
|
7
|
+
|
|
8
|
+
<a href="https://railsdesigner.com/" target="_blank">
|
|
9
|
+
<picture>
|
|
10
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Rails-Designer/mata/HEAD/.github/logo-dark.svg">
|
|
11
|
+
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/Rails-Designer/mata/HEAD/.github/logo-light.svg">
|
|
12
|
+
<img alt="Rails Designer" src="https://raw.githubusercontent.com/Rails-Designer/mata/HEAD/.github/logo-light.svg" width="240" style="max-width: 100%;">
|
|
13
|
+
</picture>
|
|
14
|
+
</a>
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
gem "mata"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
### Rails
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
# config/environments/development.rb
|
|
30
|
+
Rails.application.configure do
|
|
31
|
+
config.middleware.insert_before(
|
|
32
|
+
ActionDispatch::Static,
|
|
33
|
+
Mata,
|
|
34
|
+
watch: %w[app/views app/assets],
|
|
35
|
+
skip: %w[tmp log node_modules]
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Sinatra
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
require "sinatra"
|
|
44
|
+
require "mata"
|
|
45
|
+
|
|
46
|
+
configure :development do
|
|
47
|
+
use Mata, watch: %w[views public]
|
|
48
|
+
end
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Hanami
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
# config.ru
|
|
55
|
+
require "hanami/boot"
|
|
56
|
+
require "mata"
|
|
57
|
+
|
|
58
|
+
use Mata, watch: %w[apps lib] if Hanami.env?(:development)
|
|
59
|
+
|
|
60
|
+
run Hanami.app
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Basic Rack app
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
# config.ru
|
|
67
|
+
require "mata"
|
|
68
|
+
|
|
69
|
+
use Mata, watch: %w[views assets], skip: %w[tmp log]
|
|
70
|
+
|
|
71
|
+
run YourApp
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
## Options
|
|
76
|
+
|
|
77
|
+
- **watch**; array of paths to monitor (default: `%w[app views assets]`)
|
|
78
|
+
- **skip**; array of paths to ignore (default: `%w[tmp log]`)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
## Used in
|
|
82
|
+
|
|
83
|
+
This gem powers hot reloading in [Perron](https://github.com/rails-designer/perron), a Rails-based static site generator. It can be enabled with `config.hrm = true` in your Perron initializer.
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
## Why this gem?
|
|
87
|
+
|
|
88
|
+
I needed hot module reloading for Perron-powered Rails applications. These are minimal Rails apps typically without Hotwire or ActionCable dependencies. Existing solutions either required ActionCable (adding unnecessary complexity) or provided only basic full-page reloads without state preservation.
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
## Who is Mata?
|
|
92
|
+
|
|
93
|
+
In the smoky cabarets of Belle Époque Paris, a dancer captivated audiences with her exotic performances and mysterious allure. Born Margaretha Geertruida Zelle in the Netherlands, she had reinvented herself as an Indonesian princess, weaving tales of sacred temple dances and Eastern mystique.
|
|
94
|
+
|
|
95
|
+
She moved through the salons of Europe's elite with remarkable ease, speaking multiple languages and charming diplomats, military officers and aristocrats alike. Her lovers included high-ranking officials from opposing sides of the Great War, giving her access to secrets that others could only dream of obtaining.
|
|
96
|
+
|
|
97
|
+
Whether she was truly the master spy of legend or simply a woman caught in the wrong place at the wrong time remains a mystery. What's certain is her ability to observe, adapt and operate seamlessly across boundaries that others found impermeable.
|
|
98
|
+
|
|
99
|
+
History remembers **Mata Hari** as the ultimate double agent. She was someone who could watch, listen and report back with precision, all while maintaining perfect cover.
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
## Contributing
|
|
103
|
+
|
|
104
|
+
This project uses [Standard](https://github.com/testdouble/standard) for formatting Ruby code. Please make sure to run `be standardrb` before submitting pull requests. Run tests via `rails test`.
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
Mata is released under the [MIT License](https://opensource.org/licenses/MIT).
|
data/lib/mata/agent.rb
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Mata
|
|
4
|
+
class Agent
|
|
5
|
+
def insert(status, headers, body)
|
|
6
|
+
return [status, headers, body] unless html_response?(headers)
|
|
7
|
+
|
|
8
|
+
content = extract(body)
|
|
9
|
+
return [status, headers, [content]] unless content.include?("</head>")
|
|
10
|
+
|
|
11
|
+
script_tag = '<script src="/__mata/client.js"></script>'
|
|
12
|
+
modified_content = content.sub("</head>", "#{script_tag}\n</head>")
|
|
13
|
+
|
|
14
|
+
headers["Content-Length"] = modified_content.bytesize.to_s if headers["Content-Length"]
|
|
15
|
+
|
|
16
|
+
[status, headers, [modified_content]]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def html_response?(headers)
|
|
22
|
+
content_type = headers["Content-Type"] || headers["content-type"]
|
|
23
|
+
|
|
24
|
+
content_type&.include?("text/html")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def extract(body)
|
|
28
|
+
if body.respond_to?(:each)
|
|
29
|
+
body.to_a.join
|
|
30
|
+
else
|
|
31
|
+
body.to_s
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Mata
|
|
4
|
+
class Broadcaster
|
|
5
|
+
def initialize
|
|
6
|
+
@clients = []
|
|
7
|
+
@clients_mutex = Mutex.new
|
|
8
|
+
@cleanup_thread = cleanup_periodically
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def establish_contact(env)
|
|
12
|
+
if env["REQUEST_METHOD"] != "GET"
|
|
13
|
+
return [405, {}, []]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
headers = {
|
|
17
|
+
"Content-Type" => "text/event-stream",
|
|
18
|
+
"Cache-Control" => "no-cache",
|
|
19
|
+
"Connection" => "keep-alive",
|
|
20
|
+
"Access-Control-Allow-Origin" => "*"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
[200, headers, proc { |stream|
|
|
24
|
+
@clients_mutex.synchronize do
|
|
25
|
+
@clients << stream
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
begin
|
|
29
|
+
stream << "data: {\"type\":\"connected\"}\n\n"
|
|
30
|
+
|
|
31
|
+
Thread.new do
|
|
32
|
+
loop { sleep 30 }
|
|
33
|
+
rescue
|
|
34
|
+
@clients_mutex.synchronize { @clients.delete(stream) }
|
|
35
|
+
end
|
|
36
|
+
rescue
|
|
37
|
+
@clients_mutex.synchronize { @clients.delete(stream) }
|
|
38
|
+
end
|
|
39
|
+
}]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def deliver_payload
|
|
43
|
+
idiomorph_js = File.read(File.join(__dir__, "idiomorph.min.js"))
|
|
44
|
+
client_js = File.read(File.join(__dir__, "client.js"))
|
|
45
|
+
|
|
46
|
+
script = "#{idiomorph_js}\n\n#{client_js}"
|
|
47
|
+
|
|
48
|
+
[200, {"Content-Type" => "application/javascript"}, [script]]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def broadcast_to_all(files)
|
|
52
|
+
clients_copy = @clients_mutex.synchronize { @clients.dup }
|
|
53
|
+
return if clients_copy.empty?
|
|
54
|
+
|
|
55
|
+
files.each do |file|
|
|
56
|
+
event_data = {type: "reload"}
|
|
57
|
+
message = "data: #{event_data.to_json}\n\n"
|
|
58
|
+
|
|
59
|
+
clients_copy.each do |stream|
|
|
60
|
+
stream << message
|
|
61
|
+
rescue
|
|
62
|
+
@clients_mutex.synchronize { @clients.delete(stream) }
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def stand_down
|
|
68
|
+
@cleanup_thread&.kill
|
|
69
|
+
|
|
70
|
+
@clients_mutex.synchronize { @clients.clear }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def cleanup_periodically
|
|
76
|
+
Thread.new do
|
|
77
|
+
loop do
|
|
78
|
+
sleep 60
|
|
79
|
+
|
|
80
|
+
cleanup_dead_clients
|
|
81
|
+
end
|
|
82
|
+
rescue
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def cleanup_dead_clients
|
|
87
|
+
@clients_mutex.synchronize do
|
|
88
|
+
@clients.reject! do |client|
|
|
89
|
+
client << ""
|
|
90
|
+
false
|
|
91
|
+
rescue
|
|
92
|
+
true
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
data/lib/mata/client.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
(function() {
|
|
2
|
+
initMata();
|
|
3
|
+
|
|
4
|
+
function initMata() {
|
|
5
|
+
const eventSource = new EventSource("/__mata/events");
|
|
6
|
+
|
|
7
|
+
eventSource.onmessage = function(event) {
|
|
8
|
+
const data = JSON.parse(event.data);
|
|
9
|
+
|
|
10
|
+
switch(data.type) {
|
|
11
|
+
case "reload":
|
|
12
|
+
morphPage();
|
|
13
|
+
|
|
14
|
+
break;
|
|
15
|
+
case "connected":
|
|
16
|
+
console.log("[Mata] Connected with DOM morphing");
|
|
17
|
+
|
|
18
|
+
break;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
async function morphPage() {
|
|
23
|
+
try {
|
|
24
|
+
console.log("[Mata] Fetching updated page…");
|
|
25
|
+
const response = await fetch(window.location.href);
|
|
26
|
+
|
|
27
|
+
if (!response.ok) {
|
|
28
|
+
throw new Error(`HTTP ${response.status}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const html = await response.text();
|
|
32
|
+
const parser = new DOMParser();
|
|
33
|
+
const updatedDocument = parser.parseFromString(html, "text/html");
|
|
34
|
+
|
|
35
|
+
if (!updatedDocument.body) {
|
|
36
|
+
throw new Error("Invalid HTML response");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
Idiomorph.morph(document.documentElement, updatedDocument.documentElement, {
|
|
40
|
+
ignoreActiveValue: true,
|
|
41
|
+
callbacks: {
|
|
42
|
+
beforeNodeMorphed: function(oldNode, _) {
|
|
43
|
+
if (oldNode.tagName && oldNode.tagName.includes("-")) { // skip custom elements
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
console.log("[Mata] Page morphed successfully");
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error("[Mata] Morph failed:", error.message);
|
|
55
|
+
console.log("[Mata] Falling back to full reload");
|
|
56
|
+
|
|
57
|
+
window.location.reload();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
eventSource.onerror = function() {
|
|
62
|
+
console.log("[Mata] Connection lost, retrying…");
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
})();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var Idiomorph=function(){"use strict";const e=()=>{};const n={morphStyle:"outerHTML",callbacks:{beforeNodeAdded:e,afterNodeAdded:e,beforeNodeMorphed:e,afterNodeMorphed:e,beforeNodeRemoved:e,afterNodeRemoved:e,beforeAttributeUpdated:e},head:{style:"merge",shouldPreserve:e=>e.getAttribute("im-preserve")==="true",shouldReAppend:e=>e.getAttribute("im-re-append")==="true",shouldRemove:e,afterHeadMorphed:e},restoreFocus:true};function t(t,e,n={}){t=d(t);const r=f(e);const i=u(t,r,n);const o=a(i,()=>{return c(i,t,r,e=>{if(e.morphStyle==="innerHTML"){s(e,t,r);return Array.from(t.childNodes)}else{return l(e,t,r)}})});i.pantry.remove();return o}function l(e,t,n){const r=f(t);s(e,r,n,t,t.nextSibling);return Array.from(r.childNodes)}function a(e,t){if(!e.config.restoreFocus)return t();let n=document.activeElement;if(!(n instanceof HTMLInputElement||n instanceof HTMLTextAreaElement)){return t()}const{id:r,selectionStart:i,selectionEnd:o}=n;const l=t();if(r&&r!==document.activeElement?.getAttribute("id")){n=e.target.querySelector(`[id="${r}"]`);n?.focus()}if(n&&!n.selectionEnd&&o){n.setSelectionRange(i,o)}return l}const s=function(){function e(e,t,n,r=null,i=null){if(t instanceof HTMLTemplateElement&&n instanceof HTMLTemplateElement){t=t.content;n=n.content}r||=t.firstChild;for(const o of n.childNodes){if(r&&r!=i){const a=f(e,o,r,i);if(a){if(a!==r){h(e,r,a)}b(a,o,e);r=a.nextSibling;continue}}if(o instanceof Element){const s=o.getAttribute("id");if(e.persistentIds.has(s)){const c=p(t,s,r,e);b(c,o,e);r=c.nextSibling;continue}}const l=d(t,o,r,e);if(l){r=l.nextSibling}}while(r&&r!=i){const u=r;r=r.nextSibling;m(e,u)}}function d(e,t,n,r){if(r.callbacks.beforeNodeAdded(t)===false)return null;if(r.idMap.has(t)){const i=document.createElement(t.tagName);e.insertBefore(i,n);b(i,t,r);r.callbacks.afterNodeAdded(i);return i}else{const o=document.importNode(t,true);e.insertBefore(o,n);r.callbacks.afterNodeAdded(o);return o}}const f=function(){function e(e,t,n,r){let i=null;let o=t.nextSibling;let l=0;let a=n;while(a&&a!=r){if(c(a,t)){if(s(e,a,t)){return a}if(i===null){if(!e.idMap.has(a)){i=a}}}if(i===null&&o&&c(a,o)){l++;o=o.nextSibling;if(l>=2){i=undefined}}if(e.activeElementAndParents.includes(a))break;a=a.nextSibling}return i||null}function s(e,t,n){let r=e.idMap.get(t);let i=e.idMap.get(n);if(!i||!r)return false;for(const o of r){if(i.has(o)){return true}}return false}function c(e,t){const n=e;const r=t;return n.nodeType===r.nodeType&&n.tagName===r.tagName&&(!n.getAttribute?.("id")||n.getAttribute?.("id")===r.getAttribute?.("id"))}return e}();function m(e,t){if(e.idMap.has(t)){l(e.pantry,t,null)}else{if(e.callbacks.beforeNodeRemoved(t)===false)return;t.parentNode?.removeChild(t);e.callbacks.afterNodeRemoved(t)}}function h(t,e,n){let r=e;while(r&&r!==n){let e=r;r=r.nextSibling;m(t,e)}return r}function p(e,t,n,r){const i=r.target.getAttribute?.("id")===t&&r.target||r.target.querySelector(`[id="${t}"]`)||r.pantry.querySelector(`[id="${t}"]`);o(i,r);l(e,i,n);return i}function o(t,n){const r=t.getAttribute("id");while(t=t.parentNode){let e=n.idMap.get(t);if(e){e.delete(r);if(!e.size){n.idMap.delete(t)}}}}function l(t,n,r){if(t.moveBefore){try{t.moveBefore(n,r)}catch(e){t.insertBefore(n,r)}}else{t.insertBefore(n,r)}}return e}();const b=function(){function e(e,t,n){if(n.ignoreActive&&e===document.activeElement){return null}if(n.callbacks.beforeNodeMorphed(e,t)===false){return e}if(e instanceof HTMLHeadElement&&n.head.ignore){}else if(e instanceof HTMLHeadElement&&n.head.style!=="morph"){m(e,t,n)}else{r(e,t,n);if(!f(e,n)){s(n,e,t)}}n.callbacks.afterNodeMorphed(e,t);return e}function r(e,t,n){let r=t.nodeType;if(r===1){const i=e;const o=t;const l=i.attributes;const a=o.attributes;for(const s of a){if(d(s.name,i,"update",n)){continue}if(i.getAttribute(s.name)!==s.value){i.setAttribute(s.name,s.value)}}for(let e=l.length-1;0<=e;e--){const c=l[e];if(!c)continue;if(!o.hasAttribute(c.name)){if(d(c.name,i,"remove",n)){continue}i.removeAttribute(c.name)}}if(!f(i,n)){u(i,o,n)}}if(r===8||r===3){if(e.nodeValue!==t.nodeValue){e.nodeValue=t.nodeValue}}}function u(n,r,i){if(n instanceof HTMLInputElement&&r instanceof HTMLInputElement&&r.type!=="file"){let e=r.value;let t=n.value;o(n,r,"checked",i);o(n,r,"disabled",i);if(!r.hasAttribute("value")){if(!d("value",n,"remove",i)){n.value="";n.removeAttribute("value")}}else if(t!==e){if(!d("value",n,"update",i)){n.setAttribute("value",e);n.value=e}}}else if(n instanceof HTMLOptionElement&&r instanceof HTMLOptionElement){o(n,r,"selected",i)}else if(n instanceof HTMLTextAreaElement&&r instanceof HTMLTextAreaElement){let e=r.value;let t=n.value;if(d("value",n,"update",i)){return}if(e!==t){n.value=e}if(n.firstChild&&n.firstChild.nodeValue!==e){n.firstChild.nodeValue=e}}}function o(e,t,n,r){const i=t[n],o=e[n];if(i!==o){const l=d(n,e,"update",r);if(!l){e[n]=t[n]}if(i){if(!l){e.setAttribute(n,"")}}else{if(!d(n,e,"remove",r)){e.removeAttribute(n)}}}}function d(e,t,n,r){if(e==="value"&&r.ignoreActiveValue&&t===document.activeElement){return true}return r.callbacks.beforeAttributeUpdated(e,t,n)===false}function f(e,t){return!!t.ignoreActiveValue&&e===document.activeElement&&e!==document.body}return e}();function c(t,e,n,r){if(t.head.block){const i=e.querySelector("head");const o=n.querySelector("head");if(i&&o){const l=m(i,o,t);return Promise.all(l).then(()=>{const e=Object.assign(t,{head:{block:false,ignore:true}});return r(e)})}}return r(t)}function m(e,t,r){let i=[];let o=[];let l=[];let a=[];let s=new Map;for(const n of t.children){s.set(n.outerHTML,n)}for(const u of e.children){let e=s.has(u.outerHTML);let t=r.head.shouldReAppend(u);let n=r.head.shouldPreserve(u);if(e||n){if(t){o.push(u)}else{s.delete(u.outerHTML);l.push(u)}}else{if(r.head.style==="append"){if(t){o.push(u);a.push(u)}}else{if(r.head.shouldRemove(u)!==false){o.push(u)}}}}a.push(...s.values());let c=[];for(const d of a){let n=document.createRange().createContextualFragment(d.outerHTML).firstChild;if(r.callbacks.beforeNodeAdded(n)!==false){if("href"in n&&n.href||"src"in n&&n.src){let t;let e=new Promise(function(e){t=e});n.addEventListener("load",function(){t()});c.push(e)}e.appendChild(n);r.callbacks.afterNodeAdded(n);i.push(n)}}for(const f of o){if(r.callbacks.beforeNodeRemoved(f)!==false){e.removeChild(f);r.callbacks.afterNodeRemoved(f)}}r.head.afterHeadMorphed(e,{added:i,kept:l,removed:o});return c}const u=function(){function e(e,t,n){const{persistentIds:r,idMap:i}=f(e,t);const o=a(n);const l=o.morphStyle||"outerHTML";if(!["innerHTML","outerHTML"].includes(l)){throw`Do not understand how to morph style ${l}`}return{target:e,newContent:t,config:o,morphStyle:l,ignoreActive:o.ignoreActive,ignoreActiveValue:o.ignoreActiveValue,restoreFocus:o.restoreFocus,idMap:i,persistentIds:r,pantry:s(),activeElementAndParents:c(e),callbacks:o.callbacks,head:o.head}}function a(e){let t=Object.assign({},n);Object.assign(t,e);t.callbacks=Object.assign({},n.callbacks,e.callbacks);t.head=Object.assign({},n.head,e.head);return t}function s(){const e=document.createElement("div");e.hidden=true;document.body.insertAdjacentElement("afterend",e);return e}function c(e){let t=[];let n=document.activeElement;if(n?.tagName!=="BODY"&&e.contains(n)){while(n){t.push(n);if(n===e)break;n=n.parentElement}}return t}function u(e){let t=Array.from(e.querySelectorAll("[id]"));if(e.getAttribute?.("id")){t.push(e)}return t}function d(n,e,r,t){for(const i of t){const o=i.getAttribute("id");if(e.has(o)){let t=i;while(t){let e=n.get(t);if(e==null){e=new Set;n.set(t,e)}e.add(o);if(t===r)break;t=t.parentElement}}}}function f(e,t){const n=u(e);const r=u(t);const i=m(n,r);let o=new Map;d(o,i,e,n);const l=t.__idiomorphRoot||t;d(o,i,l,r);return{persistentIds:i,idMap:o}}function m(e,t){let n=new Set;let r=new Map;for(const{id:o,tagName:l}of e){if(r.has(o)){n.add(o)}else{r.set(o,l)}}let i=new Set;for(const{id:o,tagName:l}of t){if(i.has(o)){n.add(o)}else if(r.get(o)===l){i.add(o)}}for(const o of n){i.delete(o)}return i}return e}();const{normalizeElement:d,normalizeParent:f}=function(){const i=new WeakSet;function e(e){if(e instanceof Document){return e.documentElement}else{return e}}function r(e){if(e==null){return document.createElement("div")}else if(typeof e==="string"){return r(l(e))}else if(i.has(e)){return e}else if(e instanceof Node){if(e.parentNode){return new o(e)}else{const t=document.createElement("div");t.append(e);return t}}else{const t=document.createElement("div");for(const n of[...e]){t.append(n)}return t}}class o{constructor(e){this.originalNode=e;this.realParentNode=e.parentNode;this.previousSibling=e.previousSibling;this.nextSibling=e.nextSibling}get childNodes(){const e=[];let t=this.previousSibling?this.previousSibling.nextSibling:this.realParentNode.firstChild;while(t&&t!=this.nextSibling){e.push(t);t=t.nextSibling}return e}querySelectorAll(r){return this.childNodes.reduce((t,e)=>{if(e instanceof Element){if(e.matches(r))t.push(e);const n=e.querySelectorAll(r);for(let e=0;e<n.length;e++){t.push(n[e])}}return t},[])}insertBefore(e,t){return this.realParentNode.insertBefore(e,t)}moveBefore(e,t){return this.realParentNode.moveBefore(e,t)}get __idiomorphRoot(){return this.originalNode}}function l(n){let r=new DOMParser;let e=n.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim,"");if(e.match(/<\/html>/)||e.match(/<\/head>/)||e.match(/<\/body>/)){let t=r.parseFromString(n,"text/html");if(e.match(/<\/html>/)){i.add(t);return t}else{let e=t.firstChild;if(e){i.add(e)}return e}}else{let e=r.parseFromString("<body><template>"+n+"</template></body>","text/html");let t=e.body.querySelector("template").content;i.add(t);return t}}return{normalizeElement:e,normalizeParent:r}}();return{morph:t,defaults:n}}();
|
data/lib/mata/version.rb
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Mata
|
|
4
|
+
class WatchTower
|
|
5
|
+
def initialize(options)
|
|
6
|
+
@watch_paths = Array(options[:watch] || %w[app views assets])
|
|
7
|
+
@skip_paths = skipped_patterns(options[:skip] || options[:ignore] || %w[tmp log])
|
|
8
|
+
@on_change = nil
|
|
9
|
+
@listener = nil
|
|
10
|
+
|
|
11
|
+
observe!
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def on_change(&block)
|
|
15
|
+
@on_change = block
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def shutdown
|
|
19
|
+
@listener&.stop
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def skipped_patterns(skip_paths)
|
|
25
|
+
skip_paths.map do |path|
|
|
26
|
+
case path
|
|
27
|
+
when Regexp
|
|
28
|
+
path
|
|
29
|
+
when String
|
|
30
|
+
escaped = Regexp.escape(path)
|
|
31
|
+
/#{escaped}/
|
|
32
|
+
else
|
|
33
|
+
/#{Regexp.escape(path.to_s)}/
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def observe!
|
|
39
|
+
@last_change_time = nil
|
|
40
|
+
|
|
41
|
+
@listener = Listen.to(*@watch_paths, ignore: @skip_paths) do |modified, added, removed|
|
|
42
|
+
@last_change_time = Time.now
|
|
43
|
+
|
|
44
|
+
Thread.new do
|
|
45
|
+
sleep 0.15
|
|
46
|
+
if Time.now - @last_change_time >= 0.15
|
|
47
|
+
@on_change&.call(modified + added + removed)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
@listener.start
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
data/lib/mata.rb
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "listen"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
require "mata/agent"
|
|
7
|
+
require "mata/broadcaster"
|
|
8
|
+
require "mata/watch_tower"
|
|
9
|
+
|
|
10
|
+
class Mata
|
|
11
|
+
def initialize(app, options = {})
|
|
12
|
+
@app = app
|
|
13
|
+
|
|
14
|
+
@watch_tower = WatchTower.new(options)
|
|
15
|
+
@broadcaster = Broadcaster.new
|
|
16
|
+
@agent = Agent.new
|
|
17
|
+
|
|
18
|
+
@watch_tower.on_change { |files| @broadcaster.broadcast_to_all(files) }
|
|
19
|
+
|
|
20
|
+
at_exit { stand_down }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def call(environment)
|
|
24
|
+
request = Rack::Request.new(environment)
|
|
25
|
+
|
|
26
|
+
case request.path
|
|
27
|
+
when "/__mata/events"
|
|
28
|
+
@broadcaster.establish_contact(environment)
|
|
29
|
+
when "/__mata/client.js"
|
|
30
|
+
@broadcaster.deliver_payload
|
|
31
|
+
else
|
|
32
|
+
@agent.insert(*@app.call(environment))
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def stand_down
|
|
37
|
+
@watch_tower&.shutdown
|
|
38
|
+
@broadcaster&.stand_down
|
|
39
|
+
end
|
|
40
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: mata
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.5.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Rails Designer
|
|
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: '3.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '3.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: listen
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '3.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '3.0'
|
|
40
|
+
description: SSE-based hot reloading middleware with DOM morphing
|
|
41
|
+
email:
|
|
42
|
+
- devs@railsdesigner.com
|
|
43
|
+
executables: []
|
|
44
|
+
extensions: []
|
|
45
|
+
extra_rdoc_files: []
|
|
46
|
+
files:
|
|
47
|
+
- README.md
|
|
48
|
+
- lib/mata.rb
|
|
49
|
+
- lib/mata/agent.rb
|
|
50
|
+
- lib/mata/broadcaster.rb
|
|
51
|
+
- lib/mata/client.js
|
|
52
|
+
- lib/mata/idiomorph.min.js
|
|
53
|
+
- lib/mata/version.rb
|
|
54
|
+
- lib/mata/watch_tower.rb
|
|
55
|
+
homepage: https://railsdesigner.com/mata/
|
|
56
|
+
licenses:
|
|
57
|
+
- MIT
|
|
58
|
+
metadata:
|
|
59
|
+
homepage_uri: https://railsdesigner.com/mata/
|
|
60
|
+
source_code_uri: https://github.com/Rails-Designer/mata
|
|
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.4'
|
|
69
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - ">="
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: '0'
|
|
74
|
+
requirements: []
|
|
75
|
+
rubygems_version: 4.0.4
|
|
76
|
+
specification_version: 4
|
|
77
|
+
summary: Hot module reloading for Rack applications
|
|
78
|
+
test_files: []
|