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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b55c1eaf688f84af5c848e212ab0f5686ca6d6f270a0ef68d35846824af0d716
4
- data.tar.gz: 4ca350f60c41896911dba97cd62ec05437ede55ae11d0a6c1fd0e3a68a09281e
3
+ metadata.gz: 87cf02a737a26d181f89e3ee42f73574a30473e490b4ce045d76960e3dbc6fa5
4
+ data.tar.gz: 679a28921b7fc66630d2ec21b75543cd7dc4906d3f77230a052ff9fb08ab1281
5
5
  SHA512:
6
- metadata.gz: a434473bf2ee80768ed88e729ecec2287af5a2596521e4fdb1857e82cff861eaeeba84ac9be5b6afba41340f6de543a7c57e1382e4a6d6e19e85960f0e6ea0c6
7
- data.tar.gz: 5ddf2319739891025fac39dfc34d50a379f2403139f6171d95d2488eabf621b22dacad1266c9151af35242dff02210aef15ccc350d3b9f1c782c0c2d585f7a8b
6
+ metadata.gz: 7e25b7156a1e9d10c8d66ea198be3ddb3da0a87aa904519b6fbe0c71006e13d33f5840c051114c7a8e8506ce73f033492fb1bc4089506534759b8bc1a86d18e6
7
+ data.tar.gz: 8da9145c152a0d93d0a2d182f3ade0da334dc9bb898a75483675173f341a3ea42e5fcd1bc8e16b5e949e92b283fc1f845f7772c3fd19e193283d5338a8acd557
data/.rubocop.yml CHANGED
@@ -13,6 +13,9 @@ Style/StringLiteralsInInterpolation:
13
13
  Style/Documentation:
14
14
  Enabled: false
15
15
 
16
+ Style/GlobalVars:
17
+ Enabled: false
18
+
16
19
  Layout/LineLength:
17
20
  Max: 120
18
21
 
data/CHANGELOG.md CHANGED
@@ -1,15 +1,34 @@
1
1
  ## [Planned]
2
2
 
3
- - Server-side hot reloading
4
- - Dynamic page metadata
5
- - Client-side component rendering
6
- - Client-side routing
7
- - Websockets support
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 based to remove code duplication. Essentially any markup that has behaviour attached or would otherwise create code duplication should probably be in a Components.
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
- To start a new Porous project simply `gem install porous` using whichever Ruby environment you want to use (Ruby 3.0 minimum). Then change to that directory and run:
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 `loclahost:9292`. Now you can edit `pages/home.rb` or add more pages. Files you modify will be reloaded so you can simply refresh the page in your browser. Hot-reloading will be coming later once WebSockets support is implemented.
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
@@ -8,17 +8,14 @@ module Porous
8
8
 
9
9
  namespace :build
10
10
 
11
- desc 'build', 'Build static assets'
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
- no_commands do
19
- def transpile
20
- # TODO: Use Opal::Builder to generate pages, components and entities into static/dist
21
- end
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
@@ -16,11 +16,37 @@ module Porous
16
16
  method_option :host,
17
17
  aliases: :h,
18
18
  type: :string,
19
- default: '127.0.0.1',
19
+ default: 'localhost',
20
20
  desc: 'The host address Porous will bind to'
21
21
 
22
- def server
23
- Rackup::Server.start environment: 'development', builder: 'run Porous::Server.new'
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
@@ -3,6 +3,10 @@
3
3
  require 'thor'
4
4
 
5
5
  require 'porous'
6
+ require 'porous/server/builder'
7
+ require 'porous/server/socket'
8
+ require 'porous/server/connect'
9
+ require 'porous/server/application'
6
10
 
7
11
  require 'rack'
8
12
  require 'rackup/server'
@@ -22,14 +22,12 @@ module Porous
22
22
 
23
23
  def init_injections
24
24
  @injections ||= {}
25
- self.class.injections.each do |name, clazz|
26
- unless clazz.included_modules.include?(Porous::Injection)
27
- raise Error, "Invalid #{clazz} class, should mixin Porous::Injection"
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] = clazz
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
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Porous
4
+ class Logger < Rack::CommonLogger
5
+ def log(env, status, response_headers, began_at)
6
+ super unless env['PATH_INFO'].start_with? 'static'
7
+ end
8
+ end
9
+ end
data/lib/porous/page.rb CHANGED
@@ -9,5 +9,8 @@ module Porous
9
9
  routes.route path, to: self.class
10
10
  end
11
11
  end
12
+
13
+ def page_title = 'Porous Web'
14
+ def page_description = nil
12
15
  end
13
16
  end
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] : '' # Browser.query
103
+ @props ? @props[:query] : ''
104
104
  end
105
105
 
106
106
  def path
107
- @props ? @props[:path] : '/' # @props[:path] # Browser.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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Porous
4
- VERSION = '0.1.1'
4
+ VERSION = '0.3.0'
5
5
  end
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 'opal-virtual-dom'
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/server'
29
+ require 'porous/application'
28
30
 
29
31
  module Porous
30
32
  class Error < StandardError; end
@@ -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[svg path].freeze
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: clazz[0..-2],
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)
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Porous
4
+ class Application
5
+ include Porous::Component
6
+
7
+ inject Porous::Router, as: :router
8
+
9
+ def render
10
+ body class: 'bg-gray-50 dark:bg-gray-900' do
11
+ component router
12
+ end
13
+ end
14
+ end
15
+ end
@@ -7,10 +7,10 @@ module Porous
7
7
  new.mount_to(element)
8
8
  end
9
9
 
10
- def inject(clazz, opts = {})
11
- method_name = opts[:as] || clazz.to_s.downcase
10
+ def inject(klass, opts = {})
11
+ method_name = opts[:as] || klass.to_s.downcase
12
12
  @injections ||= {}
13
- @injections[method_name] = clazz
13
+ @injections[method_name] = klass
14
14
  end
15
15
 
16
16
  def injections
@@ -19,7 +19,7 @@ module Porous
19
19
  inject
20
20
  @virtual_dom = render_virtual_dom
21
21
  @root_node = VirtualDOM.create @virtual_dom
22
- Browser.append_child element, @root_node
22
+ element.replace_with @root_node
23
23
  self
24
24
  end
25
25
 
@@ -36,7 +36,11 @@ module Porous
36
36
  end
37
37
 
38
38
  def render!
39
- Browser.animation_frame do
39
+ if Browser::AnimationFrame.supported?
40
+ Browser::AnimationFrame.new $window do
41
+ @root_component.render_if_root
42
+ end
43
+ else
40
44
  @root_component.render_if_root
41
45
  end
42
46
  end
@@ -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.1.1
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-17 00:00:00.000000000 Z
11
+ date: 2024-02-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: listen
14
+ name: agoo
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '3.0'
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: '3.0'
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/browser.rb
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
@@ -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