sitepress-rails 5.0.0.beta2 → 5.0.0.beta3
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.
- checksums.yaml +4 -4
- data/README.md +71 -0
- data/lib/sitepress/compilers.rb +57 -0
- data/lib/sitepress/engine.rb +50 -25
- data/lib/sitepress/rails.rb +16 -1
- data/lib/sitepress/rails_configuration.rb +8 -5
- data/lib/sitepress/route_constraint.rb +49 -5
- data/lib/sitepress/routing_mapper.rb +26 -2
- data/lib/sitepress/sites.rb +88 -0
- data/lib/tasks/sitepress_tasks.rake +113 -4
- data/rails/app/controllers/concerns/sitepress/site_pages.rb +54 -10
- data/rails/app/controllers/sitepress/site_controller.rb +4 -1
- data/rails/lib/generators/sitepress/site/USAGE +26 -0
- data/rails/lib/generators/sitepress/site/site_generator.rb +103 -0
- data/rails/lib/generators/sitepress/site/templates/controller.rb.tt +3 -0
- data/rails/lib/generators/sitepress/site/templates/index.html.erb +3 -0
- data/spec/dummy/app/controllers/secondary_controller.rb +3 -0
- data/spec/dummy/app/sitepress/secondary/helpers/secondary_helper.rb +5 -0
- data/spec/dummy/app/sitepress/secondary/pages/welcome.html.erb +2 -0
- data/spec/dummy/config/initializers/sitepress.rb +7 -0
- data/spec/dummy/config/routes.rb +5 -0
- data/spec/dummy/log/test.log +11701 -0
- data/spec/sitepress/compilers_spec.rb +110 -0
- data/spec/sitepress/multi_site_integration_spec.rb +82 -0
- data/spec/sitepress/rails_configuration_spec.rb +0 -9
- data/spec/sitepress/route_constraint_spec.rb +77 -6
- data/spec/sitepress/site_generator_spec.rb +105 -0
- data/spec/sitepress/sites_spec.rb +133 -0
- metadata +26 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 443c3d5ccdfa98e1b995ae899d3d8e20dd7ffb4c98a8d5700be29384847e838d
|
|
4
|
+
data.tar.gz: a7664658ced9775dbbde217f0247933851edbbbebb6c2cd5ca8d1e893163f2fd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e092de737d65d1e5eca7eefedcdf1f1129af9872e1fdca7418c5bf0b9192fe9fd71c885698779fe120f958c1e525f734a1fd919b3e457646354bbc3eb78e8e25
|
|
7
|
+
data.tar.gz: 3eab694d5c52e70410f9f2dde9421e31f5a329aed6f83bd5d447df29df2b9b870cef72a128627810fc7552277f071d88b59aae3b0803a9436826ced73e16fab0
|
data/README.md
CHANGED
|
@@ -25,6 +25,77 @@ sitepress_root # Delete if you don't want your app's root page to be a content p
|
|
|
25
25
|
|
|
26
26
|
Restart the Rails application server and point your browser to `http://127.0.0.1:3000/` and if all went well you should see a sitepress page.
|
|
27
27
|
|
|
28
|
+
## Multiple sites in a single Rails app
|
|
29
|
+
|
|
30
|
+
You can serve any number of Sitepress sites from one Rails app — for example a marketing site at `/` and an admin docs site at `/admin/docs`. Three pieces, plain Ruby:
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
# 1. config/initializers/sitepress.rb — register the site at boot
|
|
34
|
+
Sitepress.sites << Sitepress::Site.new(root_path: "app/sitepress/admin_docs")
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
# 2. app/controllers/admin/docs_controller.rb — bind it to a controller
|
|
39
|
+
class Admin::DocsController < Sitepress::SiteController
|
|
40
|
+
self.site = Sitepress.sites.fetch("app/sitepress/admin_docs")
|
|
41
|
+
|
|
42
|
+
layout "admin"
|
|
43
|
+
before_action :require_admin
|
|
44
|
+
end
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
# 3. config/routes.rb — mount the controller
|
|
49
|
+
Rails.application.routes.draw do
|
|
50
|
+
sitepress_pages # default site at /
|
|
51
|
+
|
|
52
|
+
namespace :admin do
|
|
53
|
+
scope :docs do
|
|
54
|
+
sitepress_pages controller: "admin/docs", as: :admin_doc
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The whole multi-site API is two methods on `Sitepress`: `Sitepress.site` (the configured default, unchanged) and `Sitepress.sites` (the registry). The registry has three operations — `<<` to add, `fetch` to look up by `root_path` (raises `NotFoundError` listing registered paths on miss), and `each` plus the rest of `Enumerable` for iteration.
|
|
61
|
+
|
|
62
|
+
A typo in the path string fails loud at controller class load:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
NotFoundError: No Sitepress site registered at "app/contnet".
|
|
66
|
+
Registered: ["app/sitepress/admin_docs"]
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Why three pieces and not one?** Boot ordering forces it. Zeitwerk needs helper / model paths registered before its eager-load pass, which happens before the first request — that's what `Sitepress.sites <<` does and it's the only piece that *has* to live in an initializer. The controller binding (`self.site = ...`) is just `class_attribute` plus a writer that `prepend_view_path`s the site's view directories onto *this controller's* lookup chain (so multi-site view lookups stay local — no global ActionView pollution). Routes own the URL → controller binding, with the mount path read from the surrounding `scope`/`namespace`.
|
|
70
|
+
|
|
71
|
+
The same site can be referenced by more than one controller — a public reader and an admin editor can both `Sitepress.sites.fetch("...")` and bind to the same content tree. The Site itself is registered once.
|
|
72
|
+
|
|
73
|
+
### Generator
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
bin/rails generate sitepress:site app/sitepress/admin_docs
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Scaffolds the content directory tree (`pages/`, `helpers/`, `models/`, `assets/`), a stub index template, a controller subclass with `self.site = Sitepress.sites.fetch(...)` already filled in, and either creates or appends to `config/initializers/sitepress.rb` with the registration line. Pass `--mount-at=/admin/docs` to also inject a `scope` block into `config/routes.rb`; without the flag, the generator just prints the routes line for you to paste.
|
|
80
|
+
|
|
81
|
+
### Rake tasks
|
|
82
|
+
|
|
83
|
+
Compilation is split into single-site and multi-site forms so single-site users don't get a behavior change when they add a registered site:
|
|
84
|
+
|
|
85
|
+
- `rake sitepress:compile` — compiles the configured default site only.
|
|
86
|
+
- `rake sitepress:sites:compile` — compiles every registered site (default + `Sitepress.sites`) to `tmp/sitepress/<basename>`. Each site lives in its own subdirectory so two sites never collide on output.
|
|
87
|
+
- `rake "sitepress:sites:compile[app/sitepress/admin_docs]"` — compiles a single registered site by `root_path`. Raises `Sitepress::NotFoundError` listing registered paths if no match.
|
|
88
|
+
- `rake sitepress:sites` — lists the configured default site and everything in `Sitepress.sites`.
|
|
89
|
+
|
|
90
|
+
Two env vars adjust the compile tasks:
|
|
91
|
+
|
|
92
|
+
- `OUTPUT_PATH=build rake sitepress:sites:compile` — overrides the default `tmp/sitepress` build root.
|
|
93
|
+
- `FAIL_ON_ERROR=true rake sitepress:sites:compile` — raises on the first resource that fails to render. Default (`false`) collects all failures and prints a summary at the end.
|
|
94
|
+
|
|
95
|
+
After every compile run, the tasks print a `Compilation Summary` block listing how many sites were built, how many resources succeeded/failed, and (if any failed) the path of every failing resource paired with the site it lives in.
|
|
96
|
+
|
|
97
|
+
These tasks only handle content compilation. Run your asset bundler (Propshaft, esbuild, Tailwind, etc.) separately if you have static assets to build.
|
|
98
|
+
|
|
28
99
|
## License
|
|
29
100
|
|
|
30
101
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module Sitepress
|
|
2
|
+
# A dumb collection of compiler instances. Holds anything that
|
|
3
|
+
# responds to `#compile` (typically `Sitepress::Compiler::Files`),
|
|
4
|
+
# and runs them in order when you call `#compile`. The collection
|
|
5
|
+
# has no opinion about how its members were constructed, where they
|
|
6
|
+
# write, or what sites they're bound to — that's all the caller's
|
|
7
|
+
# job.
|
|
8
|
+
#
|
|
9
|
+
# compilers = Sitepress::Compilers.new
|
|
10
|
+
# compilers << Sitepress::Compiler::Files.new(site: foo, root_path: "build/foo")
|
|
11
|
+
# compilers << Sitepress::Compiler::Files.new(site: bar, root_path: "build/bar")
|
|
12
|
+
# compilers.compile
|
|
13
|
+
#
|
|
14
|
+
# `Enumerable` is mixed in, so `compilers.flat_map(&:succeeded)`
|
|
15
|
+
# and `compilers.flat_map(&:failed)` work for aggregating across
|
|
16
|
+
# the underlying compilers.
|
|
17
|
+
class Compilers
|
|
18
|
+
include Enumerable
|
|
19
|
+
|
|
20
|
+
def initialize(compilers = [])
|
|
21
|
+
@compilers = compilers.to_a
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Add a single compiler to the collection. Returns self per Ruby's
|
|
25
|
+
# `<<` convention so additions chain.
|
|
26
|
+
def <<(compiler)
|
|
27
|
+
@compilers << compiler
|
|
28
|
+
self
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Merge an iterable of compilers into the collection. Mirrors
|
|
32
|
+
# `Array#concat` — accepts anything responding to `#to_a` (Array,
|
|
33
|
+
# another Compilers, lazy enumerator, etc.) and adds each element
|
|
34
|
+
# individually.
|
|
35
|
+
#
|
|
36
|
+
# compilers.concat([c1, c2, c3])
|
|
37
|
+
# compilers.concat(other_compilers)
|
|
38
|
+
# compilers.concat(sites.map { |s| Compiler::Files.new(...) })
|
|
39
|
+
#
|
|
40
|
+
# Returns self for chaining.
|
|
41
|
+
def concat(other)
|
|
42
|
+
@compilers.concat(other.to_a)
|
|
43
|
+
self
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Run `#compile` on every compiler in the collection in order.
|
|
47
|
+
# Returns self for chaining.
|
|
48
|
+
def compile
|
|
49
|
+
@compilers.each(&:compile)
|
|
50
|
+
self
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def each(&block)
|
|
54
|
+
@compilers.each(&block)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
data/lib/sitepress/engine.rb
CHANGED
|
@@ -24,22 +24,27 @@ module Sitepress
|
|
|
24
24
|
"app/markdown" # When Sitepress is launched embedded in Rails project.
|
|
25
25
|
], autoload: true
|
|
26
26
|
|
|
27
|
-
# Load paths from
|
|
27
|
+
# Load paths from Sitepress sites into Rails.
|
|
28
28
|
#
|
|
29
|
-
#
|
|
29
|
+
# The work is split across two initializers because of Rails' boot
|
|
30
|
+
# ordering: the *default* site is set up before `:set_autoload_paths`
|
|
31
|
+
# (so its paths land in `config.autoload_paths` the normal way),
|
|
32
|
+
# but *registered* sites have to wait until after
|
|
33
|
+
# `:load_config_initializers` runs — that's where the user's
|
|
34
|
+
# `config/initializers/sitepress.rb` (with `Sitepress.sites << ...`)
|
|
35
|
+
# populates the registry. By the time we iterate the registry,
|
|
36
|
+
# `:set_autoload_paths` has already frozen `config.autoload_paths`,
|
|
37
|
+
# so we push paths directly to `Rails.autoloaders.main` (which
|
|
38
|
+
# accepts additions any time before eager loading).
|
|
30
39
|
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
#
|
|
38
|
-
initializer "sitepress.set_paths", before: :set_autoload_paths do |app|
|
|
40
|
+
# The default site gets views added to `app/views` globally; the
|
|
41
|
+
# default `Sitepress::SiteController` reads from there. Registered
|
|
42
|
+
# sites' views live on the controller via `prepend_view_path` so
|
|
43
|
+
# multi-site view lookups stay local to the controller.
|
|
44
|
+
|
|
45
|
+
initializer "sitepress.set_default_site_paths", before: :set_autoload_paths do |app|
|
|
39
46
|
site = Sitepress.configuration.site
|
|
40
47
|
|
|
41
|
-
# Helpers: autoloadable and available to controllers
|
|
42
|
-
# Collapsed so app/content/helpers/sample_helper.rb defines SampleHelper (not Helpers::SampleHelper)
|
|
43
48
|
site.helpers_path.expand_path.tap do |path|
|
|
44
49
|
if path.exist?
|
|
45
50
|
app.paths["app/helpers"].push path
|
|
@@ -50,8 +55,6 @@ module Sitepress
|
|
|
50
55
|
end
|
|
51
56
|
end
|
|
52
57
|
|
|
53
|
-
# Models: autoloadable
|
|
54
|
-
# Collapsed so models don't require namespace prefixes
|
|
55
58
|
site.models_path.expand_path.tap do |path|
|
|
56
59
|
if path.exist?
|
|
57
60
|
app.paths["app/models"].push path
|
|
@@ -62,21 +65,43 @@ module Sitepress
|
|
|
62
65
|
end
|
|
63
66
|
end
|
|
64
67
|
|
|
65
|
-
# Assets: available to Sprockets (no autoloading needed)
|
|
66
68
|
app.paths["app/assets"].push site.assets_path.expand_path
|
|
69
|
+
app.paths["app/views"].push site.root_path.expand_path
|
|
70
|
+
app.paths["app/views"].push site.pages_path.expand_path
|
|
71
|
+
end
|
|
67
72
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
73
|
+
initializer "sitepress.set_registered_site_paths", after: :load_config_initializers do |app|
|
|
74
|
+
# `config.autoload_paths` and `config.eager_load_paths` are frozen
|
|
75
|
+
# by the time `:load_config_initializers` finishes, but
|
|
76
|
+
# `Rails.autoloaders.main.push_dir` accepts new directories any
|
|
77
|
+
# time before eager loading. Registered sites' helpers/models
|
|
78
|
+
# become lazy-autoloadable through that path. They're not added
|
|
79
|
+
# to `eager_load_paths`, so in production they're loaded on first
|
|
80
|
+
# access rather than at boot — fine for the multi-site case
|
|
81
|
+
# where the secondary site's helpers are scoped to one controller.
|
|
82
|
+
register_helpers_late = ->(path) {
|
|
83
|
+
if path.exist?
|
|
84
|
+
Rails.autoloaders.main.push_dir(path)
|
|
85
|
+
Rails.autoloaders.main.collapse(path)
|
|
86
|
+
end
|
|
87
|
+
}
|
|
71
88
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
89
|
+
register_assets_late = ->(path) {
|
|
90
|
+
# Propshaft reads config.assets.paths lazily, so adding here
|
|
91
|
+
# works for both dev and production precompile.
|
|
92
|
+
app.config.assets.paths << path.to_s if app.config.respond_to?(:assets)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
Sitepress.configuration.sites.each do |site|
|
|
96
|
+
register_helpers_late.call site.helpers_path.expand_path
|
|
97
|
+
register_helpers_late.call site.models_path.expand_path
|
|
98
|
+
register_assets_late.call site.assets_path.expand_path
|
|
99
|
+
end
|
|
75
100
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
101
|
+
# Mark that boot-time path registration has finished. Sites#<<
|
|
102
|
+
# checks this and warns if a site is registered after this point,
|
|
103
|
+
# since its helpers/models/assets won't be picked up by Zeitwerk.
|
|
104
|
+
Sitepress.configuration.instance_variable_set(:@boot_paths_registered, true)
|
|
80
105
|
end
|
|
81
106
|
|
|
82
107
|
# Configure Sitepress with Rails settings.
|
data/lib/sitepress/rails.rb
CHANGED
|
@@ -2,11 +2,13 @@ require "sitepress-core"
|
|
|
2
2
|
|
|
3
3
|
module Sitepress
|
|
4
4
|
autoload :Compiler, "sitepress/compiler"
|
|
5
|
+
autoload :Compilers, "sitepress/compilers"
|
|
5
6
|
autoload :Model, "sitepress/model"
|
|
6
7
|
module Models
|
|
7
8
|
autoload :Collection, "sitepress/models/collection"
|
|
8
9
|
end
|
|
9
10
|
autoload :RailsConfiguration, "sitepress/rails_configuration"
|
|
11
|
+
autoload :Sites, "sitepress/sites"
|
|
10
12
|
module Renderers
|
|
11
13
|
autoload :Controller, "sitepress/renderers/controller"
|
|
12
14
|
autoload :Server, "sitepress/renderers/server"
|
|
@@ -30,11 +32,24 @@ module Sitepress
|
|
|
30
32
|
# Raised when any of the Render subclasses can't render a page.
|
|
31
33
|
RenderingError = Class.new(RuntimeError)
|
|
32
34
|
|
|
33
|
-
#
|
|
35
|
+
# The configured default site (single-site case). For multi-site
|
|
36
|
+
# apps, additional sites live in `Sitepress.sites`.
|
|
34
37
|
def self.site
|
|
35
38
|
configuration.site
|
|
36
39
|
end
|
|
37
40
|
|
|
41
|
+
# Registry of additional `Sitepress::Site` instances for multi-site
|
|
42
|
+
# apps. See `Sitepress::Sites` for the full API; the common usage is:
|
|
43
|
+
#
|
|
44
|
+
# # config/initializers/sitepress.rb
|
|
45
|
+
# Sitepress.sites << Sitepress::Site.new(root_path: "app/sitepress/admin_docs")
|
|
46
|
+
#
|
|
47
|
+
# # somewhere later (e.g. a controller class body)
|
|
48
|
+
# Sitepress.sites.fetch("app/sitepress/admin_docs")
|
|
49
|
+
def self.sites
|
|
50
|
+
configuration.sites
|
|
51
|
+
end
|
|
52
|
+
|
|
38
53
|
# Default configuration object for Sitepress Rails integration.
|
|
39
54
|
def self.configuration
|
|
40
55
|
@configuration ||= RailsConfiguration.new
|
|
@@ -14,6 +14,14 @@ module Sitepress
|
|
|
14
14
|
self.cache_resources = true
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
+
# Registry of additional `Sitepress::Site` instances for multi-site
|
|
18
|
+
# apps. Lives on the configuration object so its lifecycle is tied
|
|
19
|
+
# to the Rails app and tests can reset it via
|
|
20
|
+
# `Sitepress.reset_configuration`.
|
|
21
|
+
def sites
|
|
22
|
+
@sites ||= Sites.new
|
|
23
|
+
end
|
|
24
|
+
|
|
17
25
|
def parent_engine
|
|
18
26
|
@parent_engine ||= Rails.application
|
|
19
27
|
end
|
|
@@ -22,11 +30,6 @@ module Sitepress
|
|
|
22
30
|
@site ||= pending_site || Site.new(root_path: default_root)
|
|
23
31
|
end
|
|
24
32
|
|
|
25
|
-
# Location of Sprockets manifest file
|
|
26
|
-
def manifest_file_path
|
|
27
|
-
site.assets_path.join("config/manifest.js")
|
|
28
|
-
end
|
|
29
|
-
|
|
30
33
|
private
|
|
31
34
|
|
|
32
35
|
def pending_site
|
|
@@ -1,14 +1,58 @@
|
|
|
1
1
|
module Sitepress
|
|
2
|
-
# Route constraint for
|
|
2
|
+
# Route constraint for the Rails routes file. Two ways to construct it:
|
|
3
|
+
#
|
|
4
|
+
# - Pass `site:` directly — used by the default `sitepress_pages` route
|
|
5
|
+
# for the single-site case.
|
|
6
|
+
#
|
|
7
|
+
# - Pass `controller:` (a string like `"admin/docs"`) — the controller
|
|
8
|
+
# class is resolved lazily and its class-level `.site` method is called
|
|
9
|
+
# per request. This is what lets a controller bind itself to a site
|
|
10
|
+
# via `class_attribute :site` and have routing pick it up:
|
|
11
|
+
#
|
|
12
|
+
# class Admin::DocsController < Sitepress::SiteController
|
|
13
|
+
# self.site = Sitepress.sites.fetch("app/sitepress/admin_docs")
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# `path_prefix` is the URL prefix the route is mounted under (e.g.
|
|
17
|
+
# `/admin/docs`), stripped from `request.path` before the resource lookup.
|
|
18
|
+
# `sitepress_pages` fills this in from `@scope[:path]` automatically.
|
|
3
19
|
class RouteConstraint
|
|
4
|
-
attr_reader :
|
|
20
|
+
attr_reader :path_prefix
|
|
5
21
|
|
|
6
|
-
def initialize(site:
|
|
7
|
-
@
|
|
22
|
+
def initialize(site: nil, controller: nil, path_prefix: nil)
|
|
23
|
+
@explicit_site = site
|
|
24
|
+
@controller_name = controller
|
|
25
|
+
@path_prefix = path_prefix.presence
|
|
8
26
|
end
|
|
9
27
|
|
|
10
28
|
def matches?(request)
|
|
11
|
-
!!site.resources.get(request
|
|
29
|
+
!!site.resources.get(resource_path(request))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Resolves the site this constraint guards. If a site was passed in
|
|
33
|
+
# explicitly we use it; otherwise we resolve the controller class and
|
|
34
|
+
# ask it. Resolution is lazy because controllers may not be loaded
|
|
35
|
+
# when routes are drawn.
|
|
36
|
+
def site
|
|
37
|
+
@explicit_site || controller_class.site
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def controller_class
|
|
43
|
+
@controller_class ||= begin
|
|
44
|
+
raise ArgumentError, "Sitepress::RouteConstraint needs site: or controller:" if @controller_name.nil?
|
|
45
|
+
"#{@controller_name}_controller".camelize.constantize
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def resource_path(request)
|
|
50
|
+
path = request.path
|
|
51
|
+
if path_prefix && path.start_with?(path_prefix)
|
|
52
|
+
path = path.delete_prefix(path_prefix)
|
|
53
|
+
path = "/" if path.empty?
|
|
54
|
+
end
|
|
55
|
+
path
|
|
12
56
|
end
|
|
13
57
|
end
|
|
14
58
|
end
|
|
@@ -7,8 +7,32 @@ module ActionDispatch::Routing
|
|
|
7
7
|
DEFAULT_ACTION = "show".freeze
|
|
8
8
|
ROUTE_GLOB_KEY = "/*resource_path".freeze
|
|
9
9
|
|
|
10
|
-
# Hook up all the Sitepress pages
|
|
11
|
-
|
|
10
|
+
# Hook up all the Sitepress pages.
|
|
11
|
+
#
|
|
12
|
+
# The mount path is read from the surrounding Rails `scope`/`namespace`,
|
|
13
|
+
# so it never has to be repeated. The site is read from the controller
|
|
14
|
+
# class's `.site` method, so a multi-site app declares the site once on
|
|
15
|
+
# the controller and the routes file just mounts it.
|
|
16
|
+
#
|
|
17
|
+
# @example Default site at the root
|
|
18
|
+
# sitepress_pages
|
|
19
|
+
#
|
|
20
|
+
# @example A controller-owned site mounted under /admin/docs
|
|
21
|
+
# # app/controllers/admin/docs_controller.rb
|
|
22
|
+
# class Admin::DocsController < Sitepress::SiteController
|
|
23
|
+
# def self.site = Sitepress.site(:admin_docs)
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
# # config/routes.rb
|
|
27
|
+
# namespace :admin do
|
|
28
|
+
# scope :docs do
|
|
29
|
+
# sitepress_pages controller: "admin/docs"
|
|
30
|
+
# end
|
|
31
|
+
# end
|
|
32
|
+
def sitepress_pages(controller: DEFAULT_CONTROLLER, action: DEFAULT_ACTION, root: false, constraints: nil, as: :page)
|
|
33
|
+
path_prefix = @scope[:path].presence
|
|
34
|
+
constraints ||= Sitepress::RouteConstraint.new(controller: controller, path_prefix: path_prefix)
|
|
35
|
+
|
|
12
36
|
get ROUTE_GLOB_KEY,
|
|
13
37
|
controller: controller,
|
|
14
38
|
action: action,
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
module Sitepress
|
|
2
|
+
# Registry of `Sitepress::Site` instances for multi-site Rails apps.
|
|
3
|
+
#
|
|
4
|
+
# The whole multi-site API lives on this collection — `<<` to register,
|
|
5
|
+
# `fetch` to look up by `root_path`, `each` (and the rest of `Enumerable`)
|
|
6
|
+
# to iterate. There's intentionally no `[]`, no `add`, no `find`-by-block,
|
|
7
|
+
# and no `delete` — three methods plus Enumerable is the entire surface,
|
|
8
|
+
# so callers don't have to choose between soft-miss and hard-miss lookup
|
|
9
|
+
# forms or between `<<` and a non-chaining `add`.
|
|
10
|
+
#
|
|
11
|
+
# # config/initializers/sitepress.rb
|
|
12
|
+
# Sitepress.sites << Sitepress::Site.new(root_path: "app/sitepress/admin_docs")
|
|
13
|
+
#
|
|
14
|
+
# # somewhere later
|
|
15
|
+
# Sitepress.sites.fetch("app/sitepress/admin_docs") # => Sitepress::Site
|
|
16
|
+
# Sitepress.sites.fetch("nope") # => raises NotFoundError
|
|
17
|
+
# Sitepress.sites.each { |site| ... }
|
|
18
|
+
#
|
|
19
|
+
# The collection holds Sites directly — there's no derived key stored
|
|
20
|
+
# alongside, so the "key drift" failure mode (registry key disagreeing
|
|
21
|
+
# with the value's actual root_path) is impossible by construction.
|
|
22
|
+
# Lookup is a linear scan over `Site#root_path`, which is fine because
|
|
23
|
+
# registration is rare (boot time) and lookups happen at controller class
|
|
24
|
+
# load (also boot, in production).
|
|
25
|
+
class Sites
|
|
26
|
+
include Enumerable
|
|
27
|
+
|
|
28
|
+
def initialize
|
|
29
|
+
@sites = []
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Register a Site. Returns self per Ruby's `<<` convention so
|
|
33
|
+
# `Sitepress.sites << a << b` chains correctly.
|
|
34
|
+
#
|
|
35
|
+
# Two safety checks:
|
|
36
|
+
#
|
|
37
|
+
# - Type-checks `site` so the common mistake of pushing a path
|
|
38
|
+
# string instead of a constructed Site fails loudly at the
|
|
39
|
+
# call site rather than later when the engine tries to read
|
|
40
|
+
# `.helpers_path` on a String.
|
|
41
|
+
#
|
|
42
|
+
# - If the engine's path-setup pass has already finished (i.e.
|
|
43
|
+
# `<<` is being called from `config.after_initialize`, a
|
|
44
|
+
# request, or anywhere else that runs after initializers), the
|
|
45
|
+
# site is registered but its helpers/models/assets won't be on
|
|
46
|
+
# Zeitwerk's autoload paths. We log a warning so the user can
|
|
47
|
+
# diagnose the silent half-broken state instead of debugging
|
|
48
|
+
# "why isn't my AdminDocsHelper resolving" three days later.
|
|
49
|
+
def <<(site)
|
|
50
|
+
unless site.is_a?(Sitepress::Site)
|
|
51
|
+
raise ArgumentError,
|
|
52
|
+
"Sitepress.sites << expects a Sitepress::Site, got #{site.class}: #{site.inspect}. " \
|
|
53
|
+
"Wrap it: Sitepress.sites << Sitepress::Site.new(root_path: #{site.inspect})"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
if Sitepress.respond_to?(:configuration) &&
|
|
57
|
+
Sitepress.configuration.instance_variable_get(:@boot_paths_registered) &&
|
|
58
|
+
defined?(Rails) && Rails.logger
|
|
59
|
+
Rails.logger.warn(
|
|
60
|
+
"Sitepress.sites << #{site.root_path.inspect} called after the engine's " \
|
|
61
|
+
"path-setup pass. The site is registered, but its helpers, models, and " \
|
|
62
|
+
"assets won't be on Zeitwerk's autoload paths. Move this call into " \
|
|
63
|
+
"config/initializers/ if you want Rails to discover them."
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
@sites << site
|
|
68
|
+
self
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Find a registered Site by its `root_path`. Raises `NotFoundError`
|
|
72
|
+
# listing the registered paths if nothing matches — there's no nil
|
|
73
|
+
# return form, so a typo in the path string fails loud at the call
|
|
74
|
+
# site instead of propagating into a `NoMethodError on nil` later.
|
|
75
|
+
def fetch(root_path)
|
|
76
|
+
key = root_path.to_s
|
|
77
|
+
@sites.find { |site| site.root_path.to_s == key } || raise(
|
|
78
|
+
NotFoundError,
|
|
79
|
+
"No Sitepress site registered at #{root_path.inspect}. " \
|
|
80
|
+
"Registered: #{@sites.map { |s| s.root_path.to_s }.inspect}"
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def each(&block)
|
|
85
|
+
@sites.each(&block)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -1,4 +1,113 @@
|
|
|
1
|
-
#
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
1
|
+
# OUTPUT_PATH overrides the default tmp/sitepress build root for both
|
|
2
|
+
# `sitepress:compile` and `sitepress:sites:compile`. CI and deploy
|
|
3
|
+
# flows that want a different location (`build/`, `dist/`,
|
|
4
|
+
# `public/static/`) set OUTPUT_PATH on the invocation:
|
|
5
|
+
#
|
|
6
|
+
# OUTPUT_PATH=build rake sitepress:sites:compile
|
|
7
|
+
SITEPRESS_COMPILE_OUTPUT_ROOT = -> {
|
|
8
|
+
ENV.fetch("OUTPUT_PATH") { Rails.root.join("tmp/sitepress").to_s }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
# FAIL_ON_ERROR=true makes the compile rake tasks raise on the first
|
|
12
|
+
# resource that fails to render, aborting rake with a non-zero exit.
|
|
13
|
+
# Default (false) collects all failures and prints a summary at the
|
|
14
|
+
# end so a single bad page doesn't block the rest of the build —
|
|
15
|
+
# preferred for local iteration. Use FAIL_ON_ERROR=true in CI to make
|
|
16
|
+
# deploys notice broken pages.
|
|
17
|
+
SITEPRESS_FAIL_ON_ERROR = -> { ENV["FAIL_ON_ERROR"] == "true" }
|
|
18
|
+
|
|
19
|
+
# Build a Compiler::Files for `site`, writing into a subdirectory of
|
|
20
|
+
# `output_root` named after the site's basename. Used by both
|
|
21
|
+
# `sitepress:compile` and `sitepress:sites:compile` so they agree on
|
|
22
|
+
# the on-disk layout (each site in its own directory under the build
|
|
23
|
+
# root, no two sites colliding).
|
|
24
|
+
SITEPRESS_BUILD_COMPILER = ->(site, output_root, fail_on_error: false) {
|
|
25
|
+
Sitepress::Compiler::Files.new(
|
|
26
|
+
site: site,
|
|
27
|
+
root_path: Pathname(output_root).join(site.root_path.expand_path.basename.to_s),
|
|
28
|
+
fail_on_error: fail_on_error
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# Print a "Compilation Summary" block to stdout after a compile run.
|
|
33
|
+
# Aggregates succeeded/failed counts across every compiler in the
|
|
34
|
+
# collection and lists each failed resource with its owning site so
|
|
35
|
+
# the user can see exactly what broke.
|
|
36
|
+
SITEPRESS_PRINT_COMPILE_SUMMARY = ->(compilers) {
|
|
37
|
+
succeeded = compilers.flat_map(&:succeeded).size
|
|
38
|
+
failed = compilers.flat_map(&:failed)
|
|
39
|
+
|
|
40
|
+
puts ""
|
|
41
|
+
puts "Compilation Summary"
|
|
42
|
+
puts " Sites: #{compilers.size}"
|
|
43
|
+
puts " Succeeded: #{succeeded}"
|
|
44
|
+
puts " Failed: #{failed.size}"
|
|
45
|
+
|
|
46
|
+
if failed.any?
|
|
47
|
+
puts ""
|
|
48
|
+
puts "Failed Resources"
|
|
49
|
+
compilers.each do |compiler|
|
|
50
|
+
compiler.failed.each do |resource|
|
|
51
|
+
puts " #{compiler.site.root_path}: #{resource.request_path}"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
namespace :sitepress do
|
|
58
|
+
desc "Compile the configured default Sitepress site to OUTPUT_PATH (default: tmp/sitepress/<basename>)"
|
|
59
|
+
task compile: :environment do
|
|
60
|
+
require "sitepress/compiler"
|
|
61
|
+
output_root = SITEPRESS_COMPILE_OUTPUT_ROOT.call
|
|
62
|
+
fail_on_error = SITEPRESS_FAIL_ON_ERROR.call
|
|
63
|
+
|
|
64
|
+
compilers = Sitepress::Compilers.new
|
|
65
|
+
compilers << SITEPRESS_BUILD_COMPILER.call(Sitepress.site, output_root, fail_on_error: fail_on_error)
|
|
66
|
+
compilers.compile
|
|
67
|
+
|
|
68
|
+
SITEPRESS_PRINT_COMPILE_SUMMARY.call(compilers)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
desc "List the configured default site and all sites in Sitepress.sites"
|
|
72
|
+
task sites: :environment do
|
|
73
|
+
puts "Default:"
|
|
74
|
+
puts " #{Sitepress.site.root_path}"
|
|
75
|
+
|
|
76
|
+
if Sitepress.sites.any?
|
|
77
|
+
puts ""
|
|
78
|
+
puts "Registered (#{Sitepress.sites.count}):"
|
|
79
|
+
Sitepress.sites.each do |site|
|
|
80
|
+
puts " - #{site.root_path}"
|
|
81
|
+
end
|
|
82
|
+
else
|
|
83
|
+
puts ""
|
|
84
|
+
puts "No additional sites registered. Use Sitepress.sites << Sitepress::Site.new(root_path: ...) in an initializer."
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
namespace :sites do
|
|
89
|
+
desc "Compile every registered Sitepress site (default + Sitepress.sites) to OUTPUT_PATH (default: tmp/sitepress/<basename>)"
|
|
90
|
+
task :compile, [:root_path] => :environment do |_task, args|
|
|
91
|
+
require "sitepress/compiler"
|
|
92
|
+
output_root = SITEPRESS_COMPILE_OUTPUT_ROOT.call
|
|
93
|
+
fail_on_error = SITEPRESS_FAIL_ON_ERROR.call
|
|
94
|
+
|
|
95
|
+
# When invoked as `rake sitepress:sites:compile` we compile
|
|
96
|
+
# everything; when invoked as `rake "sitepress:sites:compile[path]"`
|
|
97
|
+
# we compile just the matching site (raising NotFoundError with
|
|
98
|
+
# the registered paths listed if no match).
|
|
99
|
+
sites = if args[:root_path]
|
|
100
|
+
[Sitepress.sites.fetch(args[:root_path])]
|
|
101
|
+
else
|
|
102
|
+
[Sitepress.site, *Sitepress.sites]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
compilers = Sitepress::Compilers.new.concat(
|
|
106
|
+
sites.map { |site| SITEPRESS_BUILD_COMPILER.call(site, output_root, fail_on_error: fail_on_error) }
|
|
107
|
+
)
|
|
108
|
+
compilers.compile
|
|
109
|
+
|
|
110
|
+
SITEPRESS_PRINT_COMPILE_SUMMARY.call(compilers)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|