staticky 0.1.1 → 0.3.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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/CHANGELOG.md +14 -0
  4. data/README.md +378 -18
  5. data/lib/staticky/application.rb +36 -0
  6. data/lib/staticky/builder.rb +4 -2
  7. data/lib/staticky/cli/commands/build.rb +13 -0
  8. data/lib/staticky/cli/commands/generate.rb +67 -0
  9. data/lib/staticky/cli/commands/version.rb +13 -0
  10. data/lib/staticky/cli/commands.rb +13 -0
  11. data/lib/staticky/cli.rb +0 -60
  12. data/lib/staticky/deps.rb +1 -1
  13. data/lib/staticky/filesystem.rb +0 -3
  14. data/lib/staticky/phlex/view_helpers.rb +12 -2
  15. data/lib/staticky/pluggable.rb +35 -0
  16. data/lib/staticky/resource.rb +14 -15
  17. data/lib/staticky/resources/plugins/phlex.rb +42 -0
  18. data/lib/staticky/resources/plugins/prelude.rb +76 -0
  19. data/lib/staticky/resources/plugins.rb +9 -0
  20. data/lib/staticky/router.rb +13 -18
  21. data/lib/staticky/routing/plugins/prelude.rb +97 -0
  22. data/lib/staticky/routing/plugins.rb +9 -0
  23. data/lib/staticky/server.rb +5 -16
  24. data/lib/staticky/server_plugin.rb +37 -0
  25. data/lib/staticky/server_plugins/live_reloading.rb +58 -0
  26. data/lib/staticky/utils.rb +63 -0
  27. data/lib/staticky/version.rb +1 -1
  28. data/lib/staticky.rb +23 -11
  29. data/site_template/.gitignore +14 -0
  30. data/site_template/.ruby-version +1 -1
  31. data/site_template/Gemfile +5 -5
  32. data/site_template/Procfile.dev +2 -2
  33. data/site_template/README.md +27 -7
  34. data/site_template/Rakefile +33 -2
  35. data/site_template/{lib/component.rb → app/views/application_component.rb} +1 -1
  36. data/site_template/app/views/application_layout.rb +4 -0
  37. data/site_template/app/views/application_page.rb +5 -0
  38. data/site_template/app/views/errors/not_found.rb +1 -1
  39. data/site_template/app/views/errors/service_error.rb +1 -1
  40. data/site_template/app/views/layouts/error.rb +4 -6
  41. data/site_template/app/views/layouts/head.rb +5 -3
  42. data/site_template/app/views/layouts/site.rb +10 -23
  43. data/site_template/app/views/pages/home.rb +1 -1
  44. data/site_template/app/views/ui/footer.rb +1 -1
  45. data/site_template/app/views/ui/navbar.rb +1 -1
  46. data/site_template/bin/{lint → setup} +8 -2
  47. data/site_template/config/boot.rb +1 -2
  48. data/site_template/config/puma.rb +11 -0
  49. data/site_template/config/staticky.rb +3 -0
  50. data/site_template/config/vite.json +3 -1
  51. data/site_template/frontend/entrypoints/application.js +0 -1
  52. data/site_template/frontend/tailwindcss/variable_font_plugin.js +1 -1
  53. data/site_template/lib/icon.rb +2 -2
  54. data/site_template/nginx.conf +8 -1
  55. data/site_template/package.json +10 -15
  56. data/site_template/tailwind.config.js +12 -8
  57. data/site_template/vite.config.ts +0 -5
  58. metadata +50 -11
  59. data/lib/staticky/container.rb +0 -26
  60. data/lib/staticky/router/definition.rb +0 -49
  61. data/lib/staticky/view_context.rb +0 -17
  62. data/site_template/frontend/turbo_transitions.js +0 -54
  63. data/site_template/lib/layout.rb +0 -4
  64. data/site_template/lib/page.rb +0 -11
data/lib/staticky/cli.rb CHANGED
@@ -4,66 +4,6 @@ require "dry/cli"
4
4
 
5
5
  module Staticky
6
6
  module CLI
7
- module Commands
8
- extend Dry::CLI::Registry
9
-
10
- class Version < Dry::CLI::Command
11
- desc "Print version"
12
-
13
- def call(*) = puts VERSION
14
- end
15
-
16
- class Build < Dry::CLI::Command
17
- desc "Build site"
18
-
19
- def call(*) = Staticky.builder.call
20
- end
21
-
22
- class Generate < Dry::CLI::Command
23
- desc "Create new site"
24
-
25
- argument :path,
26
- required: true,
27
- desc: "Relative path where the site will be generated"
28
-
29
- option :url,
30
- default: "https://example.com",
31
- desc: "Site URL",
32
- aliases: ["-u"]
33
- option :title,
34
- default: "Example",
35
- desc: "Site title",
36
- aliases: ["-t"]
37
- option :description,
38
- default: "Example site",
39
- desc: "Site description",
40
- aliases: ["-d"]
41
- option :twitter,
42
- default: "",
43
- desc: "Twitter handle",
44
- aliases: ["-t"]
45
-
46
- def call(path:, **)
47
- path = Pathname.new(path).expand_path
48
-
49
- Staticky.generator.call(path, **)
50
-
51
- commands = [
52
- "bundle install",
53
- "bundle binstubs bundler rake rspec-core vite_ruby",
54
- "yarn install",
55
- "bin/rspec"
56
- ].join(" && ")
57
-
58
- system(commands, chdir: path) || abort("install failed")
59
- end
60
- end
61
-
62
- register "version", Version
63
- register "build", Build
64
- register "new", Generate
65
- end
66
-
67
7
  def self.new(...)
68
8
  Dry::CLI.new(Commands)
69
9
  end
data/lib/staticky/deps.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Staticky
4
- Deps = Container.injector
4
+ Deps = Application.injector
5
5
  end
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "delegate"
4
- require "staticky-files"
5
-
6
3
  module Staticky
7
4
  class Filesystem < SimpleDelegator
8
5
  def self.test
@@ -3,10 +3,20 @@
3
3
  module Staticky
4
4
  module Phlex
5
5
  module ViewHelpers
6
- def link_to(text, href, **, &block) # rubocop:disable Metrics/ParameterLists
6
+ def helpers
7
+ @_view_context
8
+ end
9
+
10
+ def staticky_live_reload_js(base_path = "/")
11
+ script(type: :module) do
12
+ unsafe_raw Staticky::Utils.live_reload_js(base_path)
13
+ end
14
+ end
15
+
16
+ def link_to(text = nil, href, **, &block) # rubocop:disable Style/OptionalArguments
7
17
  block ||= proc { text }
8
18
  href = Staticky.router.resolve(href)
9
- href = href.url unless href.is_a?(String)
19
+ href = href.uri.to_s unless href.is_a?(String)
10
20
 
11
21
  a(href:, **, &block)
12
22
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Staticky
4
+ module Pluggable
5
+ class Resolver < Dry::Container::Resolver
6
+ def initialize(klass)
7
+ @klass = klass
8
+ end
9
+
10
+ def call(container, key)
11
+ container.fetch(key.to_s).call
12
+ end
13
+ end
14
+
15
+ module ClassMethods
16
+ def load_plugin(key)
17
+ resolve(key)
18
+ end
19
+
20
+ def register_plugin(key, klass)
21
+ register(key.to_s, klass)
22
+ end
23
+ end
24
+
25
+ def self.included(base)
26
+ base.extend Dry::Container::Mixin
27
+ base.extend ClassMethods
28
+ base.config.resolver = Resolver.new(base)
29
+
30
+ base.define_singleton_method :namespace do
31
+ base.name.split("::")[0..-2].join("::")
32
+ end
33
+ end
34
+ end
35
+ end
@@ -1,25 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Staticky
4
- Resource = Data.define(:url, :component) do
5
- def full_filepath
6
- Staticky.build_path.join(filepath)
7
- end
4
+ class Resource
5
+ def self.plugin(plugin, ...)
6
+ plugin = Resources::Plugins.load_plugin(plugin) if plugin.is_a?(Symbol)
7
+ unless plugin.is_a?(Module)
8
+ raise ArgumentError, "Invalid plugin type: #{plugin.class.inspect}"
9
+ end
8
10
 
9
- def read
10
- full_filepath.read
11
- end
11
+ if plugin.respond_to?(:load_dependencies)
12
+ plugin.load_dependencies(self, ...)
13
+ end
12
14
 
13
- def filepath
14
- root? ? "index.html" : "#{url}.html"
15
- end
15
+ include plugin::InstanceMethods if defined?(plugin::InstanceMethods)
16
+ extend plugin::ClassMethods if defined?(plugin::ClassMethods)
16
17
 
17
- def root?
18
- url == "/"
18
+ plugin.configure(self, ...) if plugin.respond_to?(:configure)
19
19
  end
20
20
 
21
- def build(view_context: ViewContext.new(self))
22
- component.call(view_context:)
23
- end
21
+ plugin :prelude
22
+ plugin :phlex
24
23
  end
25
24
  end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Staticky
4
+ module Resources
5
+ module Plugins
6
+ module Phlex
7
+ class ViewContext < SimpleDelegator
8
+ def initialize(resource)
9
+ super
10
+ @resource = resource
11
+ end
12
+
13
+ def root?
14
+ @resource.root?
15
+ end
16
+
17
+ def current_path
18
+ @resource.url
19
+ end
20
+ end
21
+
22
+ module InstanceMethods
23
+ def component=(component)
24
+ @component = component
25
+ end
26
+
27
+ def component
28
+ return @component if defined?(@component)
29
+
30
+ raise ArgumentError, "component is required"
31
+ end
32
+
33
+ def build(view_context: ViewContext.new(self))
34
+ component.call(view_context:)
35
+ end
36
+ end
37
+ end
38
+
39
+ register_plugin(:phlex, Phlex)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Staticky
4
+ module Resources
5
+ module Plugins
6
+ module Prelude
7
+ module ClassMethods
8
+ def new(**env)
9
+ super().tap do |resource|
10
+ env.each do |key, value|
11
+ resource.send(:"#{key}=", value)
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+ module InstanceMethods
18
+ def build_path
19
+ destination.join(filepath)
20
+ end
21
+
22
+ def filepath
23
+ return Pathname.new("index.html") if root?
24
+
25
+ Pathname.new("#{uri.path.gsub(%r{^/}, "")}.html")
26
+ end
27
+
28
+ def read
29
+ Staticky.files.read(build_path)
30
+ end
31
+
32
+ def root?
33
+ url == "/"
34
+ end
35
+
36
+ def uri
37
+ return @uri if defined?(@uri)
38
+
39
+ raise ArgumentError, "url is required"
40
+ end
41
+
42
+ def destination
43
+ @destination ||= Staticky.build_path
44
+ end
45
+
46
+ def destination=(destination)
47
+ @destination = Pathname(destination)
48
+ end
49
+
50
+ def url
51
+ return @url if defined?(@url)
52
+
53
+ raise ArgumentError, "url is required"
54
+ end
55
+
56
+ def url=(url)
57
+ @url = url
58
+ @uri = parse_url(url)
59
+ end
60
+
61
+ private
62
+
63
+ def parse_url(url)
64
+ URI(url).tap do |uri|
65
+ uri.path = "/#{uri.path}" unless uri.path.start_with?("/")
66
+ end
67
+ rescue URI::InvalidURIError => e
68
+ raise ArgumentError, e.message
69
+ end
70
+ end
71
+ end
72
+
73
+ register_plugin(:prelude, Prelude)
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Staticky
4
+ module Resources
5
+ module Plugins
6
+ include Pluggable
7
+ end
8
+ end
9
+ end
@@ -10,29 +10,24 @@ module Staticky
10
10
  # In Staticky when we build we need to do a lot of introspection to link
11
11
  # routes to resources on the file system.
12
12
 
13
- def initialize
14
- @definition = Staticky::Router::Definition.new
15
- end
13
+ Error = Class.new(Staticky::Error)
16
14
 
17
- def define(&block)
18
- tap do
19
- @definition.instance_eval(&block)
15
+ def self.plugin(plugin, ...)
16
+ plugin = Routing::Plugins.load_plugin(plugin) if plugin.is_a?(Symbol)
17
+ unless plugin.is_a?(Module)
18
+ raise ArgumentError, "Invalid plugin type: #{plugin.class.inspect}"
20
19
  end
21
- end
22
-
23
- def filepaths
24
- @definition.filepaths
25
- end
26
20
 
27
- def resources
28
- @definition.resources
29
- end
21
+ if plugin.respond_to?(:load_dependencies)
22
+ plugin.load_dependencies(self, ...)
23
+ end
30
24
 
31
- def resolve(path)
32
- # Return absolute paths as is
33
- return path if path.is_a?(String) && path.start_with?("http")
25
+ include plugin::InstanceMethods if defined?(plugin::InstanceMethods)
26
+ extend plugin::ClassMethods if defined?(plugin::ClassMethods)
34
27
 
35
- @definition.resolve(path)
28
+ plugin.configure(self, ...) if plugin.respond_to?(:configure)
36
29
  end
30
+
31
+ plugin :prelude
37
32
  end
38
33
  end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Staticky
4
+ module Routing
5
+ module Plugins
6
+ module Prelude
7
+ module ClassMethods
8
+ def new(**env)
9
+ super().tap do |router|
10
+ env.each do |key, value|
11
+ router.send(:"#{key}=", value)
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+ module InstanceMethods
18
+ def define(&block)
19
+ tap do
20
+ instance_eval(&block)
21
+ end
22
+ end
23
+
24
+ def match(path, to:, as: Resource)
25
+ path = strip_leading_slash(path)
26
+
27
+ resource = case to
28
+ when ->(x) { x.is_a?(Class) || x.is_a?(::Phlex::HTML) }
29
+ component = ensure_instance(to)
30
+ as.new(component:, url: path)
31
+ else
32
+ raise Router::Error, "Invalid route target: #{to.inspect}"
33
+ end
34
+
35
+ resources << resource
36
+ index_resource(path, resource)
37
+ end
38
+
39
+ def root(to:)
40
+ match("/", to:)
41
+ end
42
+
43
+ def resolve(path)
44
+ return path if path.is_a?(String) && path.start_with?("#")
45
+ return lookup(path) if path.is_a?(Class)
46
+
47
+ path = strip_leading_slash(path)
48
+ uri = URI(path)
49
+ # Return absolute paths as is
50
+ return path if uri.absolute?
51
+
52
+ if uri.path.size > 1 && uri.path.start_with?("/")
53
+ uri.path = uri.path[1..]
54
+ end
55
+
56
+ lookup(uri.path)
57
+ rescue URI::InvalidURIError
58
+ raise Router::Error, "Invalid path: #{path}"
59
+ rescue KeyError
60
+ raise Router::Error, "No route matches #{path}"
61
+ end
62
+
63
+ def resources = @resources ||= []
64
+ def filepaths = resources.map(&:filepath)
65
+
66
+ private
67
+
68
+ def lookup(path)
69
+ @routes_by_path.fetch(path) do
70
+ @routes_by_component.fetch(path)
71
+ end
72
+ end
73
+
74
+ def index_resource(path, resource)
75
+ routes_by_path[path] = resource
76
+ routes_by_component[resource.component.class] = resource
77
+ end
78
+
79
+ def routes_by_path = @routes_by_path ||= {}
80
+ def routes_by_component = @routes_by_component ||= {}
81
+
82
+ def ensure_instance(component)
83
+ component.is_a?(Class) ? component.new : component
84
+ end
85
+
86
+ def strip_leading_slash(path)
87
+ return path if path == "/"
88
+
89
+ path.to_s.gsub(%r{^/}, "")
90
+ end
91
+ end
92
+ end
93
+
94
+ register_plugin(:prelude, Prelude)
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Staticky
4
+ module Routing
5
+ module Plugins
6
+ include Pluggable
7
+ end
8
+ end
9
+ end
@@ -3,6 +3,7 @@
3
3
  require "roda"
4
4
 
5
5
  require_relative "../staticky"
6
+ require_relative "server_plugin"
6
7
 
7
8
  module Staticky
8
9
  class Server < Roda
@@ -12,25 +13,13 @@ module Staticky
12
13
 
13
14
  NotFound = Class.new(Staticky::Error)
14
15
 
15
- plugin :common_logger, Staticky.server_logger, method: :debug
16
- plugin :render, engine: "html"
17
- plugin :public
18
-
19
- plugin :not_found do
20
- raise NotFound if Staticky.env.test?
21
-
22
- Staticky.build_path.join("404.html").read
23
- end
24
-
25
- plugin :error_handler do |e|
26
- raise e if Staticky.env.test?
27
-
28
- Staticky.build_path.join("500.html").read
29
- end
16
+ plugin :staticky_server
30
17
 
31
18
  route do |r|
19
+ r.staticky
20
+
32
21
  Staticky.resources.each do |resource|
33
- case resource.filepath
22
+ case resource.filepath.to_s
34
23
  when "index.html"
35
24
  r.root do
36
25
  render(inline: resource.read)
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Staticky
4
+ module ServerPlugin
5
+ def self.load_dependencies(app)
6
+ app.plugin :common_logger, Staticky.server_logger, method: :debug
7
+ app.plugin :render, engine: "html"
8
+ app.plugin :public
9
+ app.plugin :exception_page
10
+
11
+ app.plugin :not_found do
12
+ raise Staticky::Server::NotFound if Staticky.env.test?
13
+
14
+ Staticky.build_path.join("404.html").read
15
+ end
16
+
17
+ app.plugin :error_handler do |error|
18
+ raise error if Staticky.env.test?
19
+ next exception_page(error) if Staticky.env.development?
20
+
21
+ Staticky.build_path.join("500.html").read
22
+ end
23
+ end
24
+
25
+ module RequestMethods
26
+ def staticky
27
+ unless Staticky.env.development? && Staticky.config.live_reloading
28
+ return
29
+ end
30
+
31
+ ServerPlugins::LiveReloading.setup_live_reload(scope)
32
+ end
33
+ end
34
+
35
+ Roda::RodaPlugins.register_plugin(:staticky_server, self)
36
+ end
37
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Staticky
4
+ module ServerPlugins
5
+ module LiveReloading
6
+ def self.setup_live_reload(app) # rubocop:disable Metrics
7
+ sleep_interval = 0.5
8
+ file_to_check = Staticky.build_path.join("index.html")
9
+ errors_file = Staticky.build_path.join("errors.json")
10
+
11
+ app.request.get "_staticky/live_reload" do # rubocop:disable Metrics/BlockLength
12
+ @_mod = if Staticky.files.exist?(file_to_check)
13
+ file_to_check.mtime.to_i
14
+ else
15
+ 0
16
+ end
17
+
18
+ event_stream = proc do |stream|
19
+ Thread.new do
20
+ loop do
21
+ new_mod = if Staticky.files.exist?(file_to_check)
22
+ file_to_check.mtime.to_i
23
+ else
24
+ 0
25
+ end
26
+
27
+ if @_mod < new_mod
28
+ stream.write "data: reloaded!\n\n"
29
+ break
30
+ elsif Staticky.files.exist?(errors_file)
31
+ stream.write "event: builderror\n" \
32
+ "data: #{errors_file.read.to_json}\n\n"
33
+ else
34
+ stream.write "data: #{new_mod}\n\n"
35
+ end
36
+
37
+ sleep sleep_interval
38
+ rescue Errno::EPIPE # User refreshed the page
39
+ break
40
+ end
41
+ ensure
42
+ stream.close
43
+ end
44
+ end
45
+
46
+ app.request.halt [
47
+ 200,
48
+ {
49
+ "Content-Type" => "text/event-stream",
50
+ "cache-control" => "no-cache"
51
+ },
52
+ event_stream
53
+ ]
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Staticky
4
+ module Utils
5
+ module_function
6
+
7
+ def live_reload_js(base_path) # rubocop:disable Metrics/MethodLength
8
+ return "" unless Staticky.env.development?
9
+
10
+ path = File.join(base_path, "/_staticky/live_reload")
11
+
12
+ <<~JAVASCRIPT
13
+ let lastmod = 0
14
+ let reconnectAttempts = 0
15
+
16
+ function statickyReload() {
17
+ if (window.Turbo) {
18
+ Turbo.visit(window.location)
19
+ } else {
20
+ location.reload()
21
+ }
22
+ }
23
+
24
+ function startLiveReload() {
25
+ const connection = new EventSource("#{path}")
26
+
27
+ connection.addEventListener("message", event => {
28
+ reconnectAttempts = 0
29
+
30
+ if (event.data == "reloaded!") {
31
+ statickyReload()
32
+ } else {
33
+ const newmod = Number(event.data)
34
+
35
+ if (lastmod < newmod) {
36
+ statickyReload()
37
+ lastmod = newmod
38
+ }
39
+ }
40
+ })
41
+
42
+ connection.addEventListener("error", () => {
43
+ if (connection.readyState === 2) {
44
+ // reconnect with new object
45
+ connection.close()
46
+ reconnectAttempts++
47
+ if (reconnectAttempts < 25) {
48
+ console.warn("Live reload: attempting to reconnect in 3 seconds...")
49
+ setTimeout(() => startLiveReload(), 3000)
50
+ } else {
51
+ console.error(
52
+ "Too many live reload connections failed. Refresh the page to try again."
53
+ )
54
+ }
55
+ }
56
+ })
57
+ }
58
+
59
+ startLiveReload()
60
+ JAVASCRIPT
61
+ end
62
+ end
63
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Staticky
4
- VERSION = "0.1.1"
4
+ VERSION = "0.3.0"
5
5
  end