staticky 0.1.1 → 0.2.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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -0
  3. data/README.md +364 -17
  4. data/lib/staticky/application.rb +36 -0
  5. data/lib/staticky/builder.rb +1 -1
  6. data/lib/staticky/cli/commands/build.rb +13 -0
  7. data/lib/staticky/cli/commands/generate.rb +67 -0
  8. data/lib/staticky/cli/commands/version.rb +13 -0
  9. data/lib/staticky/cli/commands.rb +13 -0
  10. data/lib/staticky/cli.rb +0 -60
  11. data/lib/staticky/deps.rb +1 -1
  12. data/lib/staticky/filesystem.rb +0 -3
  13. data/lib/staticky/phlex/view_helpers.rb +6 -2
  14. data/lib/staticky/pluggable.rb +35 -0
  15. data/lib/staticky/resource.rb +14 -15
  16. data/lib/staticky/resources/plugins/phlex.rb +42 -0
  17. data/lib/staticky/resources/plugins/prelude.rb +74 -0
  18. data/lib/staticky/resources/plugins.rb +9 -0
  19. data/lib/staticky/router.rb +13 -18
  20. data/lib/staticky/routing/plugins/prelude.rb +97 -0
  21. data/lib/staticky/routing/plugins.rb +9 -0
  22. data/lib/staticky/server.rb +1 -1
  23. data/lib/staticky/version.rb +1 -1
  24. data/lib/staticky.rb +20 -10
  25. data/site_template/.gitignore +14 -0
  26. data/site_template/.ruby-version +1 -1
  27. data/site_template/Gemfile +4 -3
  28. data/site_template/Procfile.dev +2 -2
  29. data/site_template/README.md +27 -7
  30. data/site_template/Rakefile +31 -2
  31. data/site_template/app/views/layouts/site.rb +2 -2
  32. data/site_template/bin/{lint → setup} +8 -2
  33. data/site_template/config/puma.rb +11 -0
  34. data/site_template/config/staticky.rb +2 -0
  35. data/site_template/lib/icon.rb +1 -1
  36. data/site_template/package.json +1 -1
  37. data/site_template/vite.config.ts +1 -1
  38. metadata +44 -7
  39. data/lib/staticky/container.rb +0 -26
  40. data/lib/staticky/router/definition.rb +0 -49
  41. data/lib/staticky/view_context.rb +0 -17
@@ -3,10 +3,14 @@
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 link_to(text = nil, href, **, &block) # rubocop:disable Style/OptionalArguments
7
11
  block ||= proc { text }
8
12
  href = Staticky.router.resolve(href)
9
- href = href.url unless href.is_a?(String)
13
+ href = href.uri.to_s unless href.is_a?(String)
10
14
 
11
15
  a(href:, **, &block)
12
16
  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,74 @@
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 filepath
19
+ destination.join(basename)
20
+ end
21
+
22
+ def read
23
+ filepath.read
24
+ end
25
+
26
+ def basename
27
+ root? ? "index.html" : "#{url}.html"
28
+ end
29
+
30
+ def root?
31
+ url == "/"
32
+ end
33
+
34
+ def uri
35
+ return @uri if defined?(@uri)
36
+
37
+ raise ArgumentError, "url is required"
38
+ end
39
+
40
+ def destination
41
+ @destination ||= Staticky.build_path
42
+ end
43
+
44
+ def destination=(destination)
45
+ @destination = Pathname(destination)
46
+ end
47
+
48
+ def url
49
+ return @url if defined?(@url)
50
+
51
+ raise ArgumentError, "url is required"
52
+ end
53
+
54
+ def url=(url)
55
+ @url = url
56
+ @uri = parse_url(url)
57
+ end
58
+
59
+ private
60
+
61
+ def parse_url(url)
62
+ URI(url).tap do |uri|
63
+ uri.path = "/#{uri.path}" unless uri.path.start_with?("/")
64
+ end
65
+ rescue URI::InvalidURIError => e
66
+ raise ArgumentError, e.message
67
+ end
68
+ end
69
+ end
70
+
71
+ register_plugin(:prelude, Prelude)
72
+ end
73
+ end
74
+ 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
@@ -30,7 +30,7 @@ module Staticky
30
30
 
31
31
  route do |r|
32
32
  Staticky.resources.each do |resource|
33
- case resource.filepath
33
+ case resource.filepath.basename.to_s
34
34
  when "index.html"
35
35
  r.root do
36
36
  render(inline: resource.read)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Staticky
4
- VERSION = "0.1.1"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/staticky.rb CHANGED
@@ -1,17 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "uri"
4
+ require "delegate"
5
+
3
6
  require "phlex"
7
+ require "dry/container"
4
8
  require "dry/system"
5
9
  require "dry/configurable"
6
10
  require "dry/logger"
7
- require "uri"
8
11
  require "tilt"
12
+ require "staticky-files"
9
13
 
10
14
  module Staticky
11
15
  GEM_ROOT = Pathname.new(__dir__).join("..").expand_path
12
16
  end
13
17
 
14
- require_relative "staticky/container"
18
+ require_relative "staticky/pluggable"
19
+ require_relative "staticky/resources/plugins"
20
+ require_relative "staticky/resources/plugins/prelude"
21
+ require_relative "staticky/resources/plugins/phlex"
22
+ require_relative "staticky/routing/plugins"
23
+ require_relative "staticky/routing/plugins/prelude"
24
+ require_relative "staticky/application"
15
25
 
16
26
  module Staticky
17
27
  # DOCS: Module for static site infrastructure such as:
@@ -24,7 +34,7 @@ module Staticky
24
34
 
25
35
  extend Dry::Configurable
26
36
 
27
- setting :env, default: :development
37
+ setting :env, default: ENV.fetch("RACK_ENV", "development").to_sym
28
38
  setting :build_path, default: Pathname.new("build")
29
39
  setting :root_path, default: Pathname(__dir__)
30
40
  setting :logger, default: Dry.Logger(:staticky, template: :details)
@@ -34,18 +44,18 @@ module Staticky
34
44
  formatter: :rack
35
45
  )
36
46
 
37
- def monitor(...) = container.monitor(...)
38
- def server_logger =config.server_logger
47
+ def monitor(...) = application.monitor(...)
48
+ def server_logger = config.server_logger
39
49
  def logger = config.logger
40
50
  def build_path = config.build_path
41
51
  def root_path = config.root_path
42
52
  def resources = router.resources
43
- def router = container[:router]
44
- def builder = container[:builder]
45
- def generator = container[:generator]
46
- def container = Container
53
+ def router = application[:router]
54
+ def builder = application[:builder]
55
+ def generator = application[:generator]
56
+ def application = Application
47
57
 
48
58
  def env
49
- @env ||= Environment.new container.env
59
+ Environment.new config.env.to_sym
50
60
  end
51
61
  end
@@ -9,3 +9,17 @@ node_modules
9
9
 
10
10
  .bundle
11
11
  /tmp/
12
+
13
+ .yarn/*
14
+ !.yarn/patches
15
+ !.yarn/plugins
16
+ !.yarn/releases
17
+ !.yarn/sdks
18
+ !.yarn/versions
19
+
20
+ # Swap the comments on the following lines if you wish to use zero-installs
21
+ # In that case, don't forget to run `yarn config set enableGlobalCache false`!
22
+ # Documentation here: https://yarnpkg.com/features/caching#zero-installs
23
+
24
+ #!.yarn/cache
25
+ .pnp.*
@@ -1 +1 @@
1
- 3.3.4
1
+ 3.3.5
@@ -13,7 +13,6 @@ gem "protos-markdown"
13
13
  gem "dry-inflector"
14
14
  gem "front_matter_parser"
15
15
  gem "rack"
16
- gem "rackup"
17
16
  gem "rake"
18
17
  gem "rouge"
19
18
  gem "staticky", github: "nolantait/staticky", branch: "master"
@@ -28,6 +27,8 @@ group :test do
28
27
  end
29
28
 
30
29
  group :development do
31
- gem "rerun"
32
- gem "rubocop-inhouse", require: false
30
+ gem "filewatcher", require: false
31
+ gem "puma"
32
+ gem "rubocop"
33
+ gem "rubocop-inhouse"
33
34
  end
@@ -1,3 +1,3 @@
1
- server: bin/bundle exec rackup
2
- builder: rerun --dir app,lib,content --exit --verbose -- bin/rake site:build
1
+ server: bin/bundle exec puma -C config/puma.rb
2
+ builder: bin/rake site:watch
3
3
  vite: bin/vite dev
@@ -9,7 +9,7 @@ assets by default and hooks into the build command defined in your `Rakefile`
9
9
  Your development server runs with `bin/dev`
10
10
 
11
11
  Everything is ruby, there is no html or erb. It outputs a static site to the
12
- `build/` folder by default, but that can be configured.
12
+ `./build` folder by default, but that can be configured.
13
13
 
14
14
  ## Usage
15
15
 
@@ -36,7 +36,7 @@ Your site should not be accessible at http://localhost:9292
36
36
 
37
37
  ## Building
38
38
 
39
- During development `rerun` watches your files and rebuilds the site when they
39
+ During development `filewatcher` watches your files and rebuilds the site when they
40
40
  change by running `bin/rake site:build`. These files are served by a Roda app.
41
41
 
42
42
  In production you simply output the files to a folder and serve them statically
@@ -46,6 +46,26 @@ can be tweaked however you like.
46
46
  Building takes all the definitions inside your `config/routes` and outputs
47
47
  static files to `./build` or wherever you have configured it.
48
48
 
49
+ ## Development and hot reloading
50
+
51
+ By default your site will use `puma` to run a `roda` server that serves the
52
+ files inside your `Staticky.build_path` (`./build` by default).
53
+
54
+ You can access your site at:
55
+
56
+ ```
57
+ http://localhost:3000
58
+ ```
59
+
60
+ You can change these settings inside your `Procfile.dev` which starts the
61
+ processes required for development.
62
+
63
+ When your site triggers a rebuild and you are connected to the page. The vite
64
+ server will trigger a page reload after 500ms.
65
+
66
+ If this is too fast and you find yourself having to refresh the page yourself
67
+ you can tweak this inside your `vite.config.ts`.
68
+
49
69
  ## Views
50
70
 
51
71
  Views are defined in `app/views`. They should be phlex components and you can
@@ -80,12 +100,12 @@ Staticky.router.define do
80
100
  # Write your own custom logic for parsing your markdown
81
101
  Dir["content/**/*.md"].each do |file|
82
102
  parsed = FrontMatterParser::Parser.parse_file(file, loader:)
103
+ basenames = file.gsub("content/", "").gsub(".md", "")
104
+ front_matter = parsed.front_matter.transform_keys(&:to_sym)
83
105
 
84
- match file.gsub("content/", "").gsub(".md", ""),
85
- to: Pages::Post.new(
86
- parsed.content,
87
- front_matter: parsed.front_matter.transform_keys(&:to_sym)
88
- )
106
+ basename.each do |path|
107
+ match path, to: Pages::Post.new(parsed.content, front_matter:)
108
+ end
89
109
  end
90
110
  end
91
111
  ```
@@ -7,12 +7,41 @@ ViteRuby.install_tasks
7
7
  desc "Precompile assets"
8
8
  task :environment do
9
9
  require "./config/boot"
10
+
11
+ Staticky.application.monitor(:builder, methods: %i[call]) do |event|
12
+ Staticky.logger.info "Built site in #{event[:time]}ms"
13
+ end
10
14
  end
11
15
 
12
16
  namespace :site do
13
- desc "Precompile assets"
17
+ desc "Build the site and its assets into the Staticky.build_path (./build)"
14
18
  task build: :environment do
15
- Rake::Task["vite:build"].invoke
19
+ Staticky.logger.info "Building site in #{Staticky.env.name} environment..."
16
20
  Staticky.builder.call
17
21
  end
22
+
23
+ desc "Watch the site and its assets for changes"
24
+ task watch: :environment do
25
+ require "filewatcher"
26
+
27
+ Rake::Task["site:build"].execute unless Staticky.build_path.exist?
28
+
29
+ Staticky.logger.info "Watching site in #{Staticky.env.name} environment..."
30
+
31
+ Filewatcher.new(
32
+ [
33
+ "app/**/*.rb",
34
+ "lib/**/*.rb",
35
+ "content/**/*"
36
+ ]
37
+ ).watch do
38
+ Staticky.logger.info "Change detected, rebuilding site..."
39
+ sh("bin/rake site:build") do |ok, res|
40
+ unless ok
41
+ Staticky.logger.error "Error rebuilding site:"
42
+ puts res
43
+ end
44
+ end
45
+ end
46
+ end
18
47
  end
@@ -28,11 +28,11 @@ module Layouts
28
28
  private
29
29
 
30
30
  def content
31
- @content || proc {}
31
+ @content || proc { }
32
32
  end
33
33
 
34
34
  def head
35
- @head || proc {}
35
+ @head || proc { }
36
36
  end
37
37
  end
38
38
  end
@@ -13,6 +13,12 @@ FileUtils.chdir APP_ROOT do
13
13
  # This script is idempotent, so that you can run it at any time and get an expectable outcome.
14
14
  # Add necessary setup steps to this file.
15
15
 
16
- puts "== Running rubocop =="
17
- system! "bundle exec rubocop --autocorrect-all --fail-level error"
16
+ puts "== Installing dependencies =="
17
+ system! "gem install bundler --conservative"
18
+ system("bundle check") || system!("bundle install")
19
+
20
+ system! "bundle binstubs --force bundler rubocop rspec-core vite_ruby rake"
21
+
22
+ puts "\n== Installing javascript dependencies =="
23
+ system! "yarn install"
18
24
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Run in single threaded mode
4
+ threads 0, 1
5
+
6
+ # Specifies the `port` that Puma will listen on to receive requests; default is
7
+ # 3000.
8
+ port 3000
9
+
10
+ # Only use a pidfile when requested
11
+ pidfile ENV["PIDFILE"] if ENV["PIDFILE"]
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  Staticky.configure do |config|
2
4
  config.build_path = Pathname.new("build")
3
5
  config.root_path = Pathname(__dir__).join("..")
@@ -2,7 +2,7 @@
2
2
 
3
3
  class Icon < Component
4
4
  param :name, reader: false
5
- option :variant, reader: false, default: -> {}
5
+ option :variant, reader: false, default: -> { }
6
6
  option :size, default: -> { :md }, reader: false
7
7
 
8
8
  def template
@@ -27,5 +27,5 @@
27
27
  "optionalDependencies": {
28
28
  "@rollup/rollup-linux-arm64-musl": "4.20.0"
29
29
  },
30
- "packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610"
30
+ "packageManager": "yarn@4.5.0"
31
31
  }