porous 0.1.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -0
- data/CHANGELOG.md +25 -6
- data/README.md +17 -7
- data/lib/porous/application.rb +32 -0
- data/lib/porous/cli/build.rb +6 -9
- data/lib/porous/cli/server.rb +29 -3
- data/lib/porous/cli/template/pages/home.rb +3 -1
- data/lib/porous/cli.rb +4 -0
- data/lib/porous/injection.rb +4 -6
- data/lib/porous/logger.rb +9 -0
- data/lib/porous/page.rb +3 -0
- data/lib/porous/router.rb +2 -2
- data/lib/porous/server/application.rb +28 -0
- data/lib/porous/server/builder.rb +52 -0
- data/lib/porous/server/connect.rb +18 -0
- data/lib/porous/server/socket.rb +39 -0
- data/lib/porous/version.rb +1 -1
- data/lib/porous.rb +5 -3
- data/lib/virtual_dom/dom.rb +5 -4
- data/opal/porous/application.rb +15 -0
- data/opal/porous/component/class_methods.rb +3 -3
- data/opal/porous/component.rb +1 -1
- data/opal/porous/injection.rb +5 -1
- data/opal/porous/page.rb +16 -0
- data/opal/porous/router.rb +153 -0
- data/opal/porous/routes.rb +94 -0
- data/opal/porous.rb +40 -0
- metadata +15 -7
- data/lib/porous/server.rb +0 -72
- data/opal/porous/browser.rb +0 -39
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 87cf02a737a26d181f89e3ee42f73574a30473e490b4ce045d76960e3dbc6fa5
|
4
|
+
data.tar.gz: 679a28921b7fc66630d2ec21b75543cd7dc4906d3f77230a052ff9fb08ab1281
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7e25b7156a1e9d10c8d66ea198be3ddb3da0a87aa904519b6fbe0c71006e13d33f5840c051114c7a8e8506ce73f033492fb1bc4089506534759b8bc1a86d18e6
|
7
|
+
data.tar.gz: 8da9145c152a0d93d0a2d182f3ade0da334dc9bb898a75483675173f341a3ea42e5fcd1bc8e16b5e949e92b283fc1f845f7772c3fd19e193283d5338a8acd557
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,15 +1,34 @@
|
|
1
1
|
## [Planned]
|
2
2
|
|
3
|
-
-
|
4
|
-
-
|
5
|
-
-
|
6
|
-
-
|
7
|
-
|
3
|
+
- Production mode
|
4
|
+
- Data Abstraction Layer / Object Relational Model
|
5
|
+
- Event Model
|
6
|
+
- Plugin / Extension system
|
7
|
+
|
8
|
+
- Frontend Extensions
|
9
|
+
- Tailwind CSS (tailwind-cli)
|
10
|
+
|
11
|
+
- Persistence Extensions
|
12
|
+
- Memory (default)
|
13
|
+
- Disk (file)
|
14
|
+
- Databases (SQLite, PostgreSQL)
|
15
|
+
|
16
|
+
## [0.3.0] - 22 February 2024
|
17
|
+
|
18
|
+
- WebSockets support
|
19
|
+
|
20
|
+
## [0.2.0] - 18 February 2024
|
21
|
+
|
22
|
+
- Server-side hot reloading (browser reloads on changes)
|
23
|
+
- Dynamic page metadata (title and description)
|
24
|
+
- Less noisy logging (silence logging for Rack::Static)
|
25
|
+
- Client-side component rendering (sans SVG support)
|
26
|
+
- Client-side routing / navigation
|
8
27
|
- Client-side hot reloading
|
9
28
|
|
10
29
|
## [0.1.1] - 17 February 2024
|
11
30
|
|
12
|
-
- Server-side reloading
|
31
|
+
- Server-side reloading (server requests reflect changes)
|
13
32
|
|
14
33
|
## [0.1.0] - 15 February 2024
|
15
34
|
|
data/README.md
CHANGED
@@ -1,11 +1,19 @@
|
|
1
|
-
# Porous
|
1
|
+
# 🧽 Porous
|
2
2
|
|
3
|
-
Porous is a web engine that uses isomorphic Ruby components to build a Progressive Web App. Its use is analogous to a web framework, but the approach is entirely different.
|
3
|
+
Porous is a web engine that uses isomorphic Ruby components to build a Progressive Web App. Its use is analogous to a web framework, but the approach is entirely different. You write *only* the code that is *unique to your application* and the engine takes care of the rest!
|
4
4
|
|
5
|
-
This project is a work-in-progress and is not yet even in the Proof of Concept phase. However, if you are interested in a full-stack, everything included solution, that only requires you to use one language (that is arguably easy and enjoyable to write) then feel free to follow this project.
|
5
|
+
This project is a **work-in-progress** and is not yet even in the Proof of Concept phase. However, if you are interested in a full-stack, everything included solution, that only requires you to use one language (that is arguably easy and enjoyable to write) then feel free to follow this project.
|
6
6
|
|
7
7
|
The closest thing to this I could find was [Volt](https://github.com/voltrb/volt) or [Silica](https://github.com/youchan/silica), neither of which are active or match the overall development flow I'm looking for.
|
8
8
|
|
9
|
+
## Current Features
|
10
|
+
|
11
|
+
- 🙅 No bundled runtime (only code unique to your app needs to be in your repository)
|
12
|
+
- 🖥️ Server-side rendering (server responds with the entire initial page populated for SEO)
|
13
|
+
- 💻 Client-side rendering (application bundle is served and interactions and subsequent pages are rendered client-side)
|
14
|
+
- 🌄 Serves static files (from `static` folder)
|
15
|
+
- 🔥 Hot reloading (via WebSocket push and browser refresh)
|
16
|
+
|
9
17
|
## Design
|
10
18
|
|
11
19
|
Applications are composed of `Page`s which are in turn composed of `Component`s. Data is persisted as `Entity`s in configurable store options (memory, disk, database). Client-server communication occurs as `Event`s over WebSockets.
|
@@ -16,7 +24,7 @@ A page is conceptually similar to what would be rendered when visiting a specifi
|
|
16
24
|
|
17
25
|
### Components
|
18
26
|
|
19
|
-
A component is any composable unit of code responsible for rendering markup, potentially based on some state. This is somewhat equivalent to Web Components, in that it can also have some behaviour attached. But it can also simply be
|
27
|
+
A component is any composable unit of code responsible for rendering markup, potentially based on some state. This is somewhat equivalent to Web Components, in that it can also have some behaviour attached. But it can also simply be used to remove code duplication. Essentially any markup that has behaviour attached or would otherwise create code duplication should probably be in Components.
|
20
28
|
|
21
29
|
### Entities
|
22
30
|
|
@@ -36,11 +44,13 @@ Porous is not a framework. You don't build an application with it as a dependenc
|
|
36
44
|
|
37
45
|
Porous is still pre-alpha and so is not ready for usage yet, but the general idea is that you would define your application's entities, pages, components and events in Ruby scripts structured in a specific way. Then you would simply run `porous` while pointing it to that folder and it will spin up a Rack-compatible web server for you to use.
|
38
46
|
|
39
|
-
|
47
|
+
> ⚠️ Expect any and all APIs to change radically until version 1.0! Hence why it won't be documented or properly tested until things settle to a more stable state.
|
48
|
+
|
49
|
+
To start a new Porous project simply `gem install porous` using whichever Ruby environment you want to use (Ruby 3.0+). Then change to that directory and run:
|
40
50
|
|
41
51
|
$ porous server
|
42
52
|
|
43
|
-
By default Porous will run at `
|
53
|
+
By default Porous will run at `localhost:9292`. Now you can edit `pages/home.rb` or add more pages. Files you modify will be hot-reloaded so you can simply open the page in your browser and edit the file. Hot-reloading will be improved once WebSockets support is implemented.
|
44
54
|
|
45
55
|
### Running examples
|
46
56
|
|
@@ -66,4 +76,4 @@ Everyone interacting in the Porous project's codebases, issue trackers, chat roo
|
|
66
76
|
|
67
77
|
## Acknowledgements
|
68
78
|
|
69
|
-
I'd like to thank Michał Kalbarczyk ([fazibear](https://github.com/fazibear)) for his work done on [Inesita](https://github.com/inesita-rb/inesita) and his [VirtualDOM wrapper](https://github.com/fazibear/opal-virtual-dom) which served as the starting point for my implementation of Porous.
|
79
|
+
I'd like to thank Michał Kalbarczyk ([fazibear](https://github.com/fazibear)) for his work done on [Inesita](https://github.com/inesita-rb/inesita) and his [VirtualDOM wrapper](https://github.com/fazibear/opal-virtual-dom) which served as the starting point for my implementation of Porous. While my final approach may deviate significantly from theirs, having code to review and a workable starting point was invaluable.
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Porous
|
4
|
+
class Application
|
5
|
+
include Porous::Component
|
6
|
+
|
7
|
+
# rubocop:disable Metrics/AbcSize
|
8
|
+
def render
|
9
|
+
html do
|
10
|
+
head do
|
11
|
+
meta charset: 'UTF-8'
|
12
|
+
meta name: 'viewport', content: 'width=device-width, initial-scale=1.0'
|
13
|
+
|
14
|
+
if props[:title]
|
15
|
+
title do
|
16
|
+
text props[:title]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
meta name: 'description', content: props[:description] if props[:description]
|
20
|
+
|
21
|
+
script src: '/static/dist/application.js', id: 'application'
|
22
|
+
script src: 'https://cdn.tailwindcss.com'
|
23
|
+
end
|
24
|
+
|
25
|
+
body class: 'bg-gray-50 dark:bg-gray-900' do
|
26
|
+
component Porous::Router, props: { path: props[:path], query: props[:query] }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
# rubocop:enable Metrics/AbcSize
|
31
|
+
end
|
32
|
+
end
|
data/lib/porous/cli/build.rb
CHANGED
@@ -8,17 +8,14 @@ module Porous
|
|
8
8
|
|
9
9
|
namespace :build
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
def build
|
14
|
-
empty_directory 'static/dist', force: options[:force]
|
15
|
-
transpile
|
11
|
+
def self.exit_on_failure?
|
12
|
+
true
|
16
13
|
end
|
17
14
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
15
|
+
desc 'build', 'Build static assets'
|
16
|
+
def build
|
17
|
+
empty_directory 'static/dist', verbose: false, force: options[:force]
|
18
|
+
Porous::Server::Builder.new.build
|
22
19
|
end
|
23
20
|
end
|
24
21
|
end
|
data/lib/porous/cli/server.rb
CHANGED
@@ -16,11 +16,37 @@ module Porous
|
|
16
16
|
method_option :host,
|
17
17
|
aliases: :h,
|
18
18
|
type: :string,
|
19
|
-
default: '
|
19
|
+
default: 'localhost',
|
20
20
|
desc: 'The host address Porous will bind to'
|
21
21
|
|
22
|
-
def server
|
23
|
-
|
22
|
+
def server # rubocop:todo Metrics/MethodLength
|
23
|
+
Agoo::Log.configure(dir: '',
|
24
|
+
console: true,
|
25
|
+
classic: true,
|
26
|
+
colorize: true,
|
27
|
+
states: {
|
28
|
+
INFO: true,
|
29
|
+
DEBUG: false,
|
30
|
+
connect: false,
|
31
|
+
request: false,
|
32
|
+
response: false,
|
33
|
+
eval: true,
|
34
|
+
push: true
|
35
|
+
})
|
36
|
+
|
37
|
+
Agoo::Server.init 9292, Dir.pwd, thread_count: 1
|
38
|
+
Agoo::Server.use Rack::ContentLength
|
39
|
+
Agoo::Server.use Rack::Static, urls: ['/static']
|
40
|
+
Agoo::Server.use Rack::ShowExceptions
|
41
|
+
|
42
|
+
# Socket Communication
|
43
|
+
$socket ||= Porous::Server::Socket.new
|
44
|
+
Agoo::Server.handle nil, '/connect', Porous::Server::Connect.new
|
45
|
+
# Server-Side Rendering
|
46
|
+
Agoo::Server.handle nil, '**', Porous::Server::Application.new
|
47
|
+
Agoo::Server.start
|
48
|
+
# Live Reload Builder
|
49
|
+
Server::Builder.new.build.start
|
24
50
|
end
|
25
51
|
end
|
26
52
|
end
|
@@ -5,6 +5,8 @@ class Home
|
|
5
5
|
include Porous::Component
|
6
6
|
|
7
7
|
def route = '/'
|
8
|
+
def page_title = 'Porous Web | Home'
|
9
|
+
def page_description = 'Landing page generated by Porous'
|
8
10
|
|
9
11
|
# rubocop:disable Metrics, Layout/LineLength
|
10
12
|
def render
|
@@ -23,7 +25,7 @@ class Home
|
|
23
25
|
div class: 'flex flex-col items-start space-y-3 sm:space-x-4 sm:space-y-0 sm:items-center sm:flex-row' do
|
24
26
|
a href: 'https://github.com/exastencil/porous', target: '_blank', rel: 'noopener',
|
25
27
|
class: 'group relative inline-flex h-12 items-center justify-center overflow-hidden rounded-md bg-indigo-600 px-6 font-medium text-neutral-200 transition hover:scale-110' do
|
26
|
-
span 'Get Started'
|
28
|
+
span 'Get Started 🧽'
|
27
29
|
div class: 'absolute inset-0 flex h-full w-full justify-center [transform:skew(-12deg)_translateX(-100%)] group-hover:duration-1000 group-hover:[transform:skew(-12deg)_translateX(100%)]' do
|
28
30
|
div class: 'relative h-full w-8 bg-white/20'
|
29
31
|
end
|
data/lib/porous/cli.rb
CHANGED
data/lib/porous/injection.rb
CHANGED
@@ -22,14 +22,12 @@ module Porous
|
|
22
22
|
|
23
23
|
def init_injections
|
24
24
|
@injections ||= {}
|
25
|
-
self.class.injections.each do |name,
|
26
|
-
unless
|
27
|
-
raise Error, "Invalid #{
|
25
|
+
self.class.injections.each do |name, klass|
|
26
|
+
unless klass.included_modules.include?(Porous::Injection)
|
27
|
+
raise Error, "Invalid #{klass} class, should mixin Porous::Injection"
|
28
28
|
end
|
29
29
|
|
30
|
-
@injections[name] =
|
31
|
-
.new
|
32
|
-
.with_root_component(@root_component)
|
30
|
+
@injections[name] = klass.new.with_root_component(@root_component)
|
33
31
|
end
|
34
32
|
@injections.each_value do |instance|
|
35
33
|
instance.inject
|
data/lib/porous/page.rb
CHANGED
data/lib/porous/router.rb
CHANGED
@@ -100,11 +100,11 @@ module Porous
|
|
100
100
|
end
|
101
101
|
|
102
102
|
def query
|
103
|
-
@props ? @props[:query] : ''
|
103
|
+
@props ? @props[:query] : ''
|
104
104
|
end
|
105
105
|
|
106
106
|
def path
|
107
|
-
@props ? @props[:path] : '/'
|
107
|
+
@props ? @props[:path] : '/'
|
108
108
|
end
|
109
109
|
|
110
110
|
def current_url?(name)
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Porous
|
4
|
+
module Server
|
5
|
+
class Application
|
6
|
+
MONITORING = %w[components pages].freeze
|
7
|
+
|
8
|
+
def call(env)
|
9
|
+
router = Porous::Router.new path: env['PATH_INFO'], query: env['QUERY_STRING']
|
10
|
+
route = router.find_route
|
11
|
+
page = route[:component].new(route[:params])
|
12
|
+
|
13
|
+
[200, { 'content-type' => 'text/html' }, [
|
14
|
+
Porous::Application.new(
|
15
|
+
title: page.page_title,
|
16
|
+
description: page.page_description,
|
17
|
+
path: env['PATH_INFO'],
|
18
|
+
query: env['QUERY_STRING']
|
19
|
+
).to_s
|
20
|
+
]]
|
21
|
+
rescue Porous::InvalidRouteError => e
|
22
|
+
[404, { 'content-type' => 'text/plain' }, ["404 Page not found\n", e.message]]
|
23
|
+
rescue Porous::Error => e
|
24
|
+
[500, { 'content-type' => 'text/plain' }, ["500 Internal Server Error\n", e.message]]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'opal/builder_scheduler/sequential'
|
4
|
+
|
5
|
+
module Porous
|
6
|
+
module Server
|
7
|
+
class Builder
|
8
|
+
def initialize
|
9
|
+
@build_queue = Queue.new
|
10
|
+
@last_build = nil
|
11
|
+
@latest_change = Dir.glob(File.join('**', '*.rb')).map { |f| File.mtime f }.max
|
12
|
+
end
|
13
|
+
|
14
|
+
def build
|
15
|
+
components = Dir.glob(File.join('**', '*.rb')).map do |relative_path|
|
16
|
+
modified = File.mtime relative_path
|
17
|
+
@latest_change = modified if modified > @latest_change
|
18
|
+
"require '#{relative_path}'"
|
19
|
+
end
|
20
|
+
build_string = "require 'porous'; #{components.join ";"}".gsub '.rb', ''
|
21
|
+
builder = Opal::Builder.new scheduler: Opal::BuilderScheduler::Sequential, cache: false
|
22
|
+
builder.build_str build_string, '(inline)'
|
23
|
+
File.binwrite "#{Dir.pwd}/static/dist/application.js", builder.to_s
|
24
|
+
@last_build = Time.now
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
# rubocop:disable Metrics/AbcSize
|
29
|
+
def start
|
30
|
+
loop do
|
31
|
+
sleep 0.25
|
32
|
+
next unless @build_queue.empty?
|
33
|
+
|
34
|
+
modified = Dir.glob(File.join('**', '*.rb')).map { |f| File.mtime f }.max
|
35
|
+
next unless modified > @last_build
|
36
|
+
|
37
|
+
@build_queue.push modified
|
38
|
+
# Load for server
|
39
|
+
Dir.glob(File.join('**', '*.rb')).map { |f| load File.expand_path("#{Dir.pwd}/#{f}") }
|
40
|
+
|
41
|
+
# Rebuild for browser
|
42
|
+
Thread.new { build }.join
|
43
|
+
|
44
|
+
# Notify clients
|
45
|
+
$socket.public 'build', @last_build.inspect
|
46
|
+
@build_queue.clear
|
47
|
+
end
|
48
|
+
end
|
49
|
+
# rubocop:enable Metrics/AbcSize
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Porous
|
4
|
+
module Server
|
5
|
+
class Connect
|
6
|
+
# Only used for WebSocket or SSE upgrades.
|
7
|
+
def call(env)
|
8
|
+
if env['rack.upgrade?'].nil?
|
9
|
+
[404, {}, []]
|
10
|
+
else
|
11
|
+
$socket ||= Socket.new
|
12
|
+
env['rack.upgrade'] = $socket
|
13
|
+
[200, {}, []]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Porous
|
4
|
+
module Server
|
5
|
+
class Socket
|
6
|
+
def initialize
|
7
|
+
@clients = []
|
8
|
+
@mutex = Mutex.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def on_open(client)
|
12
|
+
@mutex.synchronize do
|
13
|
+
@clients << client
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def on_close(client)
|
18
|
+
@mutex.synchronize do
|
19
|
+
@clients.delete(client)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def on_drained(_client); end
|
24
|
+
|
25
|
+
def on_message(client, data)
|
26
|
+
client.write("Handler says #{data}")
|
27
|
+
end
|
28
|
+
|
29
|
+
def public(channel, message)
|
30
|
+
output = "#{channel}|#{message}"
|
31
|
+
@mutex.synchronize do
|
32
|
+
@clients.each do |c|
|
33
|
+
c.write output
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/porous/version.rb
CHANGED
data/lib/porous.rb
CHANGED
@@ -2,10 +2,12 @@
|
|
2
2
|
|
3
3
|
require 'opal'
|
4
4
|
require 'opal-browser'
|
5
|
+
require 'opal-virtual-dom'
|
6
|
+
|
5
7
|
Opal.append_path File.expand_path('../opal', __dir__)
|
8
|
+
Opal.append_path File.expand_path(Dir.pwd)
|
6
9
|
|
7
|
-
require '
|
8
|
-
require 'listen'
|
10
|
+
require 'agoo'
|
9
11
|
|
10
12
|
require 'porous/version'
|
11
13
|
|
@@ -24,7 +26,7 @@ Dir.glob(File.join('{components,pages}', '**', '*.rb')).each do |relative_path|
|
|
24
26
|
require File.expand_path("#{Dir.pwd}/#{relative_path}")
|
25
27
|
end
|
26
28
|
|
27
|
-
require 'porous/
|
29
|
+
require 'porous/application'
|
28
30
|
|
29
31
|
module Porous
|
30
32
|
class Error < StandardError; end
|
data/lib/virtual_dom/dom.rb
CHANGED
@@ -11,8 +11,9 @@ module VirtualDOM
|
|
11
11
|
small source span strong style sub summary sup table tbody td textarea tfoot th
|
12
12
|
thead time title tr track u ul var video wbr].freeze
|
13
13
|
|
14
|
-
SVG_TAGS = %w[
|
15
|
-
|
14
|
+
SVG_TAGS = %w[animate animateMotion animateTransform circle clipPath defs desc ellipse filter
|
15
|
+
foreignObject g image line linearGradient marker mask metadata mpath path pattern
|
16
|
+
polygon polyline radialGradient rect set stop svg switch symbol textPath tspan use view].freeze
|
16
17
|
(HTML_TAGS + SVG_TAGS).each do |tag|
|
17
18
|
define_method tag do |params = {}, &block|
|
18
19
|
if params.is_a?(String)
|
@@ -55,10 +56,10 @@ module VirtualDOM
|
|
55
56
|
|
56
57
|
class_params = @__last_virtual_node__.params.delete(:className)
|
57
58
|
method_params = if klass.end_with?('!')
|
58
|
-
{ id:
|
59
|
+
{ id: klass[0..-2],
|
59
60
|
class: merge_string(class_params, params[:class]) }
|
60
61
|
else
|
61
|
-
{ class: merge_string(class_params, params[:class], klass.gsub('_', '-').gsub('--', '_')) }
|
62
|
+
{ class: merge_string(class_params, params[:class], klass.to_s.gsub('_', '-').gsub('--', '_')) }
|
62
63
|
end
|
63
64
|
params = @__last_virtual_node__.params.merge(params).merge(method_params)
|
64
65
|
process_tag(@__last_virtual_node__.name, params, block, children)
|
@@ -7,10 +7,10 @@ module Porous
|
|
7
7
|
new.mount_to(element)
|
8
8
|
end
|
9
9
|
|
10
|
-
def inject(
|
11
|
-
method_name = opts[:as] ||
|
10
|
+
def inject(klass, opts = {})
|
11
|
+
method_name = opts[:as] || klass.to_s.downcase
|
12
12
|
@injections ||= {}
|
13
|
-
@injections[method_name] =
|
13
|
+
@injections[method_name] = klass
|
14
14
|
end
|
15
15
|
|
16
16
|
def injections
|
data/opal/porous/component.rb
CHANGED
data/opal/porous/injection.rb
CHANGED
data/opal/porous/page.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Porous
|
4
|
+
module Page
|
5
|
+
# Define the route according to the Router::Routes rules
|
6
|
+
def route!
|
7
|
+
path = route
|
8
|
+
@route ||= Routes.new.tap do |routes|
|
9
|
+
routes.route path, to: self.class
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def page_title = 'Porous Web'
|
14
|
+
def page_description = nil
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Porous
|
4
|
+
# rubocop:disable Metrics/ClassLength
|
5
|
+
class Router
|
6
|
+
include Porous::Component
|
7
|
+
|
8
|
+
attr_reader :params
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@routes = Routes.new
|
12
|
+
# Extract the routes from all Pages
|
13
|
+
# Object.descendants.each { |klass| puts klass if klass.is_a?(Class) }
|
14
|
+
Object.descendants.select { |c| c.is_a?(Class) && c.included_modules.include?(Porous::Page) }.each do |klass|
|
15
|
+
# puts "Included in: #{klass}"
|
16
|
+
# next if klass.to_s.start_with? '#<Class:' # skip singleton classes
|
17
|
+
|
18
|
+
@routes.combine klass.new.route!
|
19
|
+
end
|
20
|
+
|
21
|
+
raise Error, 'No Porous::Page components found!' if @routes.routes.empty?
|
22
|
+
|
23
|
+
find_route
|
24
|
+
parse_url_params
|
25
|
+
add_listeners
|
26
|
+
end
|
27
|
+
|
28
|
+
# Handle anchor tags with router (requires router to be injected)
|
29
|
+
Component.module_eval do
|
30
|
+
unless respond_to?(:__a)
|
31
|
+
alias_method :__a, :a
|
32
|
+
define_method(:a) do |params = {}, &block|
|
33
|
+
if params['href'].include?('//') || params['target'] == '_blank'
|
34
|
+
# Retain behaviour
|
35
|
+
else
|
36
|
+
href = params['href']
|
37
|
+
params[:onclick] = lambda { |e|
|
38
|
+
e.prevent
|
39
|
+
raise Error, 'No router to handle navigation. Did you `inject Porous::Router`' unless router
|
40
|
+
|
41
|
+
router.go_to(href)
|
42
|
+
}
|
43
|
+
end
|
44
|
+
__a(params, &block)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def add_listeners
|
50
|
+
return unless Browser::History.supported?
|
51
|
+
|
52
|
+
$window.on(:popstate) do
|
53
|
+
find_route
|
54
|
+
parse_url_params
|
55
|
+
render!
|
56
|
+
end
|
57
|
+
$window.on(:hashchange) do
|
58
|
+
find_route
|
59
|
+
parse_url_params
|
60
|
+
render!
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def route(*params, &block)
|
65
|
+
@routes.route(*params, &block)
|
66
|
+
end
|
67
|
+
|
68
|
+
def find_route
|
69
|
+
@routes.routes.each do |route|
|
70
|
+
next unless path.match(route[:regex])
|
71
|
+
return go_to(url_for(route[:redirect_to])) if route[:redirect_to]
|
72
|
+
|
73
|
+
return @route = route
|
74
|
+
end
|
75
|
+
raise Error, "Can't find route for url"
|
76
|
+
end
|
77
|
+
|
78
|
+
def find_component(route)
|
79
|
+
call_on_enter_callback(route)
|
80
|
+
@component_props = route[:component_props]
|
81
|
+
route[:component]
|
82
|
+
end
|
83
|
+
|
84
|
+
def render
|
85
|
+
component find_component(@route), props: @component_props if @route
|
86
|
+
end
|
87
|
+
|
88
|
+
def call_on_enter_callback(route)
|
89
|
+
return unless route[:on_enter]
|
90
|
+
|
91
|
+
return unless route[:on_enter].respond_to?(:call)
|
92
|
+
|
93
|
+
route[:on_enter].call
|
94
|
+
end
|
95
|
+
|
96
|
+
def go_to(path)
|
97
|
+
$window.history.push path
|
98
|
+
find_route
|
99
|
+
parse_url_params
|
100
|
+
render!
|
101
|
+
false
|
102
|
+
end
|
103
|
+
|
104
|
+
def parse_url_params
|
105
|
+
@params = component_url_params
|
106
|
+
return if query.empty?
|
107
|
+
|
108
|
+
query[1..].split('&').each do |param|
|
109
|
+
key, value = param.split('=')
|
110
|
+
@params[Browser.decode_uri_component(key)] = Browser.decode_uri_component(value)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def component_url_params
|
115
|
+
@route[:params].zip(path.match(@route[:regex])[1..]).to_h
|
116
|
+
end
|
117
|
+
|
118
|
+
def url_for(name, params = nil)
|
119
|
+
route = @routes.routes.find do |r|
|
120
|
+
case name
|
121
|
+
when String
|
122
|
+
r[:name] == name || r[:path] == name
|
123
|
+
when Object
|
124
|
+
r[:component] == name
|
125
|
+
else
|
126
|
+
false
|
127
|
+
end
|
128
|
+
end
|
129
|
+
route ? url_with_params(route, params) : raise(Error, "Route '#{name}' not found.")
|
130
|
+
end
|
131
|
+
|
132
|
+
def query
|
133
|
+
$window.location.query
|
134
|
+
end
|
135
|
+
|
136
|
+
def path
|
137
|
+
$window.location.path
|
138
|
+
end
|
139
|
+
|
140
|
+
def current_url?(name)
|
141
|
+
path == url_for(name, params)
|
142
|
+
end
|
143
|
+
|
144
|
+
def url_with_params(route, params)
|
145
|
+
path = route[:path]
|
146
|
+
params&.each do |key, value|
|
147
|
+
path = path.gsub(":#{key}", value.to_s)
|
148
|
+
end
|
149
|
+
path
|
150
|
+
end
|
151
|
+
end
|
152
|
+
# rubocop:enable Metrics/ClassLength
|
153
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Porous
|
4
|
+
class Routes
|
5
|
+
attr_reader :routes
|
6
|
+
|
7
|
+
def initialize(parent = nil)
|
8
|
+
@parent = parent
|
9
|
+
@routes = []
|
10
|
+
end
|
11
|
+
|
12
|
+
# rubocop:disable Metrics/AbcSize
|
13
|
+
def route(*params, &block)
|
14
|
+
path = params.first.gsub(%r{^/}, '')
|
15
|
+
path = @parent ? "#{@parent}/#{path}" : "/#{path}"
|
16
|
+
|
17
|
+
add_subroutes(path, &block) if block_given?
|
18
|
+
|
19
|
+
if params.last[:redirect_to]
|
20
|
+
add_redirect(path, params.last[:redirect_to])
|
21
|
+
else
|
22
|
+
add_route(params.last[:as], path, params.last[:to], params.last[:props], params.last[:on_enter])
|
23
|
+
end
|
24
|
+
end
|
25
|
+
# rubocop:enable Metrics/AbcSize
|
26
|
+
|
27
|
+
def validate_component(component)
|
28
|
+
raise Error, 'Component not exists' unless component
|
29
|
+
|
30
|
+
return if component.include?(Porous::Component)
|
31
|
+
|
32
|
+
raise Error,
|
33
|
+
"Invalid #{component} class, should mixin Porous::Component"
|
34
|
+
end
|
35
|
+
|
36
|
+
def add_redirect(path, redirect_to)
|
37
|
+
@routes << {
|
38
|
+
path: path,
|
39
|
+
redirect_to: redirect_to
|
40
|
+
}.merge(build_params_and_regex(path))
|
41
|
+
end
|
42
|
+
|
43
|
+
def add_route(name, path, component, component_props, on_enter)
|
44
|
+
validate_component(component)
|
45
|
+
@routes << {
|
46
|
+
path: path,
|
47
|
+
component: component,
|
48
|
+
component_props: component_props,
|
49
|
+
on_enter: on_enter,
|
50
|
+
name: name || component.to_s.gsub(/(.)([A-Z])/, '\1_\2').downcase
|
51
|
+
}.merge(build_params_and_regex(path))
|
52
|
+
end
|
53
|
+
|
54
|
+
def add_subroutes(path, &block)
|
55
|
+
subroutes = Routes.new(path)
|
56
|
+
subroutes.instance_exec(&block)
|
57
|
+
@routes += subroutes.routes
|
58
|
+
end
|
59
|
+
|
60
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
61
|
+
def build_params_and_regex(path)
|
62
|
+
regex = ['^']
|
63
|
+
params = []
|
64
|
+
parts = path.split('/')
|
65
|
+
regex << '\/' if parts.empty?
|
66
|
+
parts.each do |part|
|
67
|
+
next if part.empty?
|
68
|
+
|
69
|
+
regex << '\/'
|
70
|
+
case part[0]
|
71
|
+
when ':'
|
72
|
+
params << part[1..]
|
73
|
+
regex << '([^\/]+)'
|
74
|
+
when '*'
|
75
|
+
params << part[1..]
|
76
|
+
regex << '(.*)'
|
77
|
+
break
|
78
|
+
else
|
79
|
+
regex << part
|
80
|
+
end
|
81
|
+
end
|
82
|
+
regex << '$'
|
83
|
+
{
|
84
|
+
regex: Regexp.new(regex.join),
|
85
|
+
params: params
|
86
|
+
}
|
87
|
+
end
|
88
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
89
|
+
|
90
|
+
def combine(other)
|
91
|
+
@routes += other.routes
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
data/opal/porous.rb
CHANGED
@@ -10,3 +10,43 @@ require 'console'
|
|
10
10
|
|
11
11
|
require 'virtual_dom'
|
12
12
|
require 'virtual_dom/support/browser'
|
13
|
+
|
14
|
+
require 'porous/injection'
|
15
|
+
require 'porous/component/class_methods'
|
16
|
+
require 'porous/component/render'
|
17
|
+
require 'porous/component/virtual'
|
18
|
+
require 'porous/component'
|
19
|
+
require 'porous/page'
|
20
|
+
|
21
|
+
require 'porous/routes'
|
22
|
+
require 'porous/router'
|
23
|
+
require 'porous/application'
|
24
|
+
|
25
|
+
module Porous
|
26
|
+
class Error < StandardError; end
|
27
|
+
class InvalidRouteError < Error; end
|
28
|
+
end
|
29
|
+
|
30
|
+
$document.ready do
|
31
|
+
Porous::Application.mount_to($document.body)
|
32
|
+
Browser::Socket.new 'ws://localhost:9292/connect' do
|
33
|
+
on :open do |_e|
|
34
|
+
$console.info 'Connected to server!'
|
35
|
+
end
|
36
|
+
|
37
|
+
on :message do |e|
|
38
|
+
channel, content = e.data.split '|'
|
39
|
+
case channel
|
40
|
+
when 'build'
|
41
|
+
if content == 'started'
|
42
|
+
$console.log 'New build started…'
|
43
|
+
else
|
44
|
+
$console.log 'Reloading scripts…'
|
45
|
+
$document.location.reload
|
46
|
+
end
|
47
|
+
else
|
48
|
+
$console.log "Received #{e.data}"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
metadata
CHANGED
@@ -1,29 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: porous
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Exa Stencil
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-02-
|
11
|
+
date: 2024-02-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: agoo
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '2.15'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
26
|
+
version: '2.15'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: opal-browser
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -99,6 +99,7 @@ files:
|
|
99
99
|
- Rakefile
|
100
100
|
- exe/porous
|
101
101
|
- lib/porous.rb
|
102
|
+
- lib/porous/application.rb
|
102
103
|
- lib/porous/cli.rb
|
103
104
|
- lib/porous/cli/build.rb
|
104
105
|
- lib/porous/cli/new.rb
|
@@ -112,20 +113,27 @@ files:
|
|
112
113
|
- lib/porous/component/render.rb
|
113
114
|
- lib/porous/component/virtual.rb
|
114
115
|
- lib/porous/injection.rb
|
116
|
+
- lib/porous/logger.rb
|
115
117
|
- lib/porous/page.rb
|
116
118
|
- lib/porous/router.rb
|
117
119
|
- lib/porous/routes.rb
|
118
|
-
- lib/porous/server.rb
|
120
|
+
- lib/porous/server/application.rb
|
121
|
+
- lib/porous/server/builder.rb
|
122
|
+
- lib/porous/server/connect.rb
|
123
|
+
- lib/porous/server/socket.rb
|
119
124
|
- lib/porous/version.rb
|
120
125
|
- lib/virtual_dom/dom.rb
|
121
126
|
- lib/virtual_dom/virtual_node.rb
|
122
127
|
- opal/porous.rb
|
123
|
-
- opal/porous/
|
128
|
+
- opal/porous/application.rb
|
124
129
|
- opal/porous/component.rb
|
125
130
|
- opal/porous/component/class_methods.rb
|
126
131
|
- opal/porous/component/render.rb
|
127
132
|
- opal/porous/component/virtual.rb
|
128
133
|
- opal/porous/injection.rb
|
134
|
+
- opal/porous/page.rb
|
135
|
+
- opal/porous/router.rb
|
136
|
+
- opal/porous/routes.rb
|
129
137
|
- sig/porous.rbs
|
130
138
|
homepage: https://github.com/exastencil/porous
|
131
139
|
licenses:
|
data/lib/porous/server.rb
DELETED
@@ -1,72 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Porous
|
4
|
-
class Application
|
5
|
-
include Porous::Component
|
6
|
-
|
7
|
-
def render
|
8
|
-
html do
|
9
|
-
head do
|
10
|
-
title do
|
11
|
-
text props[:title]
|
12
|
-
end
|
13
|
-
meta charset: 'UTF-8'
|
14
|
-
meta name: 'viewport', content: 'width=device-width, initial-scale=1.0'
|
15
|
-
script src: 'https://cdn.tailwindcss.com'
|
16
|
-
end
|
17
|
-
|
18
|
-
body class: 'bg-gray-50 dark:bg-gray-900' do
|
19
|
-
component Porous::Router, props: { path: props[:path], query: props[:query] }
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
class Server
|
26
|
-
MONITORING = %w[components pages].freeze
|
27
|
-
|
28
|
-
def initialize(*_args)
|
29
|
-
@queue = Queue.new
|
30
|
-
start_live_reload
|
31
|
-
setup_rack_app
|
32
|
-
end
|
33
|
-
|
34
|
-
def start_live_reload
|
35
|
-
MONITORING.each { |path| FileUtils.mkdir_p path }
|
36
|
-
opts = {
|
37
|
-
only: /\.rb$/,
|
38
|
-
relative: true
|
39
|
-
}
|
40
|
-
@listener = Listen.to(*MONITORING, opts) do |modified, added, _removed|
|
41
|
-
(modified + added).each do |file|
|
42
|
-
load File.expand_path("#{Dir.pwd}/#{file}")
|
43
|
-
end
|
44
|
-
setup_rack_app
|
45
|
-
end
|
46
|
-
@listener.start
|
47
|
-
at_exit { @listener.stop }
|
48
|
-
end
|
49
|
-
|
50
|
-
def setup_rack_app
|
51
|
-
@rack = Rack::Builder.new do
|
52
|
-
use Rack::Static, urls: ['/static']
|
53
|
-
run do |env|
|
54
|
-
# Build a router to check for a valid route
|
55
|
-
Porous::Router.new path: env['PATH_INFO'], query: env['QUERY_STRING']
|
56
|
-
[200, { 'content-type' => 'text/html' },
|
57
|
-
[Application.new(title: 'Porous Web', path: env['PATH_INFO'], query: env['QUERY_STRING']).to_s]]
|
58
|
-
rescue Porous::InvalidRouteError => e
|
59
|
-
[404, { 'content-type' => 'text/plain' },
|
60
|
-
["404 Page not found\n", e.message]]
|
61
|
-
rescue Porous::Error => e
|
62
|
-
[500, { 'content-type' => 'text/plain' },
|
63
|
-
["500 Internal Server Error\n", e.message]]
|
64
|
-
end
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
def call(*args)
|
69
|
-
@rack.call(*args)
|
70
|
-
end
|
71
|
-
end
|
72
|
-
end
|
data/opal/porous/browser.rb
DELETED
@@ -1,39 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Porous
|
4
|
-
module Browser
|
5
|
-
module_function
|
6
|
-
|
7
|
-
Window = JS.global
|
8
|
-
Document = Window.JS[:document]
|
9
|
-
AddEventListener = Window.JS[:addEventListener]
|
10
|
-
|
11
|
-
if Native(Window.JS[:requestAnimationFrame])
|
12
|
-
def animation_frame(&block)
|
13
|
-
Window.JS.requestAnimationFrame(block)
|
14
|
-
end
|
15
|
-
else
|
16
|
-
def animation_frame(&block)
|
17
|
-
block.call
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
def ready?(&block)
|
22
|
-
AddEventListener.call('load', block)
|
23
|
-
end
|
24
|
-
|
25
|
-
def body
|
26
|
-
Document.JS[:body]
|
27
|
-
end
|
28
|
-
|
29
|
-
def append_child(node, new_node)
|
30
|
-
node = node.to_n unless native?(node)
|
31
|
-
new_node = new_node.to_n unless native?(new_node)
|
32
|
-
node.JS.appendChild(new_node)
|
33
|
-
end
|
34
|
-
|
35
|
-
def query_element(css)
|
36
|
-
Document.JS.querySelector(css)
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|