staticky 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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
  }