bridgetown-routes 1.3.4 → 2.0.0.beta1

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: 16347dadf3ce286824c397b0407d648ebf4f50e72780d0932b74038235e78c27
4
- data.tar.gz: b27b57fa4edf117798108ad0396e9e83ae39634656bdd5244a571bf5d175a70e
3
+ metadata.gz: 304b389006048955c739fc4162c3f678f0425388f126d13f1507a71c0f207333
4
+ data.tar.gz: ce03add76d718c225da566b472ceab910d0193743549713fb5664e44a9f7e581
5
5
  SHA512:
6
- metadata.gz: bde4592c7d79d3129e089c992c9b9e8aebb86361b3671b76772aebea37215b9df9340dbf3a5e179986df936437cdbc9acf92361741933d10f252ba66bf16d12a
7
- data.tar.gz: 4b9468c44bf67027e6b7b860af1c18acd8517ba40bb866546324180e91f12314101ead50c8a64d8d27e49776ca4108f710f015b7b6e827e0ca24e0cd4fb1c88c
6
+ metadata.gz: 0ec8b61de86e46cf4aebf44171ec1421403bd5522a46ff1f0fc93c3cb1fc36d6b277751eecff130cf25f5daeec344528b5b712bd90f978649e97e56d3269d794
7
+ data.tar.gz: fd97a1dbaad47125d3cf69b38d1bc58695ec666f6ff4fcee6009366a74ce401188eef92d8ff740d01a1389bc995b903ccf74abd56770084063e5b073892b053b
data/.rubocop.yml CHANGED
@@ -4,3 +4,4 @@ inherit_from: ../.rubocop.yml
4
4
  AllCops:
5
5
  Exclude:
6
6
  - "*.gemspec"
7
+ - "test/ssr/src/**/*.rb"
@@ -6,8 +6,11 @@ module Bridgetown
6
6
  class << self
7
7
  attr_accessor :blocks
8
8
 
9
- def add_route(name, file_code = nil, &block)
9
+ def add_route(name, file_code = nil, front_matter_line_count = nil, &block)
10
10
  block.instance_variable_set(:@_route_file_code, file_code) if file_code
11
+ if front_matter_line_count
12
+ block.instance_variable_set(:@_front_matter_line_count, front_matter_line_count)
13
+ end
11
14
 
12
15
  @blocks ||= {}
13
16
  @blocks[name] = block
@@ -30,18 +33,23 @@ module Bridgetown
30
33
 
31
34
  code = File.read(file)
32
35
  code_postmatch = nil
33
- ruby_content = code.match(Bridgetown::FrontMatterImporter::RUBY_BLOCK)
36
+ ruby_content = code.match(Bridgetown::FrontMatter::Loaders::Ruby::BLOCK)
37
+ front_matter_line_count = nil
34
38
  if ruby_content
35
39
  code = ruby_content[1]
36
- code_postmatch = ruby_content.post_match
40
+ code_postmatch = ruby_content.post_match.lstrip
41
+ front_matter_line_count = code.lines.count - 1
42
+ if code.match?(%r!^\s*render_with(\s|\()!).! && code.match?(%r!r\.[a-z]+\s+do!).!
43
+ code.concat("\nrender_with {}")
44
+ end
37
45
  end
38
46
 
39
- code = <<~RUBY
40
- add_route(#{file_slug.inspect}, #{code_postmatch.inspect}) do |r|
41
- #{code}
42
- end
43
- RUBY
44
- instance_eval(code, file, ruby_content ? 1 : 0)
47
+ # rubocop:disable Style/DocumentDynamicEvalDefinition, Style/EvalWithLocation
48
+ code_proc = Kernel.eval(
49
+ "proc {|r| #{code} }", TOPLEVEL_BINDING, file, ruby_content ? 2 : 1
50
+ )
51
+ add_route(file_slug, code_postmatch, front_matter_line_count, &code_proc)
52
+ # rubocop:enable Style/DocumentDynamicEvalDefinition, Style/EvalWithLocation
45
53
  end
46
54
  end
47
55
  end
@@ -2,97 +2,111 @@
2
2
 
3
3
  module Bridgetown
4
4
  module Routes
5
- module Manifest
6
- class << self
7
- def generate_manifest(site) # rubocop:todo Metrics/AbcSize, Metrics/CyclomaticComplexity
8
- return @route_manifest[site.label] if @route_manifest && Bridgetown.env.production?
5
+ class Manifest
6
+ attr_reader :site, :config, :manifest
7
+
8
+ def initialize(site, cache_routes: Bridgetown.env.production?)
9
+ @site = site
10
+ @manifest = []
11
+ @config = site.config.routes
12
+ @cache_routes = cache_routes
13
+ @islands_dir = File.expand_path(site.config.islands_dir, site.config.source)
14
+ end
9
15
 
10
- new_manifest = []
11
- routable_extensions = site.config.routes.extensions.join(",")
16
+ def routable_extensions = config.extensions.join(",")
12
17
 
13
- site.config.routes.source_paths.each do |routes_dir|
14
- routes_dir = File.expand_path(routes_dir, site.config.source)
18
+ def routes
19
+ return @manifest if !@manifest.empty? && @cache_routes
15
20
 
16
- # @type [Array]
17
- routes = Dir.glob(
18
- routes_dir + "/**/*.{#{routable_extensions}}"
19
- ).filter_map do |file|
20
- if File.basename(file).start_with?("_", ".") ||
21
- File.basename(file, ".*").end_with?(".test")
22
- next
23
- end
21
+ @manifest = []
24
22
 
25
- file_slug, segment_keys = file_slug_and_segments(site, routes_dir, file)
23
+ # Loop through all the directories (`src/_routes`, etc) looking for route files, then
24
+ # sort them and add them to the manifest:
25
+ expand_source_paths_with_islands.each do |routes_dir|
26
+ @manifest += glob_routes(routes_dir).map do |file|
27
+ file_slug, segment_keys = file_slug_and_segments(routes_dir, file)
26
28
 
27
- # generate localized file slugs
28
- localized_file_slugs = generate_localized_file_slugs(site, file_slug)
29
+ # generate localized file slugs
30
+ localized_file_slugs = generate_localized_file_slugs(file_slug)
29
31
 
30
- [file, localized_file_slugs, segment_keys]
31
- end
32
+ [file, localized_file_slugs, segment_keys]
33
+ end.then { sort_routes! _1 }
34
+ end
32
35
 
33
- new_manifest += sort_routes!(routes)
34
- end
36
+ @manifest
37
+ end
38
+
39
+ def expand_source_paths_with_islands
40
+ # clear out any past islands folders
41
+ config.source_paths.reject! { _1.start_with?(@islands_dir) }
35
42
 
36
- @route_manifest ||= {}
37
- @route_manifest[site.label] = new_manifest
43
+ Dir.glob("#{@islands_dir}/**/routes").each do |route_folder|
44
+ config.source_paths << route_folder
38
45
  end
39
46
 
40
- def sort_routes!(routes)
41
- routes.sort! do |route_a, route_b|
42
- # @type [String]
43
- slug_a = route_a[1][0]
44
- # @type [String]
45
- slug_b = route_b[1][0]
46
-
47
- # @type [Integer]
48
- weight1 = slug_a.count("/") <=> slug_b.count("/")
49
- if weight1.zero?
50
- slug_b.count("/:") <=> slug_a.count("/:")
51
- else
52
- weight1
53
- end
54
- end.reverse!
47
+ config.source_paths.map { File.expand_path _1, site.config.source }
48
+ end
49
+
50
+ def glob_routes(dir, pattern = "**/*")
51
+ files = Dir.glob("#{dir}/#{pattern}.{#{routable_extensions}}")
52
+ files.reject! do |file|
53
+ File.basename(file, ".*").then { _1.start_with?("_", ".") || _1.end_with?(".test") }
54
+ end
55
+ files
56
+ end
57
+
58
+ def file_slug_and_segments(routes_dir, file)
59
+ # @type [String]
60
+ file_slug = file.delete_prefix("#{routes_dir}/").then do |f|
61
+ if routes_dir.start_with?(@islands_dir)
62
+ # convert _islands/foldername/routes/someroute.rb to foldername/someroute.rb
63
+ f = routes_dir.delete_prefix("#{@islands_dir}/").sub(%r!/routes$!, "/") + f
64
+ end
65
+ [File.dirname(f), File.basename(f, ".*")].join("/").delete_prefix("./")
66
+ end.delete_suffix("/index")
67
+ segment_keys = []
68
+ file_slug.gsub!(%r{\[([^/]+)\]}) do |_segment|
69
+ segment_keys << Regexp.last_match(1)
70
+ ":#{Regexp.last_match(1)}"
55
71
  end
56
72
 
57
- def locale_for(slug, site)
58
- possible_locale_segment = slug.split("/").first.to_sym
73
+ [file_slug, segment_keys]
74
+ end
59
75
 
60
- if site.config.available_locales.include? possible_locale_segment
61
- possible_locale_segment
76
+ def generate_localized_file_slugs(file_slug)
77
+ site.config.available_locales.map do |locale|
78
+ if locale == site.config.default_locale && !site.config.prefix_default_locale
79
+ file_slug
62
80
  else
63
- site.config.default_locale
81
+ "#{locale}/#{file_slug}"
64
82
  end
65
83
  end
84
+ end
66
85
 
67
- private
68
-
69
- def file_slug_and_segments(site, routes_dir, file)
86
+ def sort_routes!(routes)
87
+ routes.sort! do |route_a, route_b|
88
+ # @type [String]
89
+ slug_a = route_a[1][0]
70
90
  # @type [String]
71
- file_slug = file.delete_prefix("#{routes_dir}/").then do |f|
72
- if routes_dir.start_with?(
73
- File.expand_path(site.config[:islands_dir], site.config.source)
74
- )
75
- f = "#{site.config[:islands_dir].delete_prefix("_")}/#{f}"
76
- end
77
- [File.dirname(f), File.basename(f, ".*")].join("/").delete_prefix("./")
78
- end.delete_suffix("/index")
79
- segment_keys = []
80
- file_slug.gsub!(%r{\[([^/]+)\]}) do |_segment|
81
- segment_keys << Regexp.last_match(1)
82
- ":#{Regexp.last_match(1)}"
91
+ slug_b = route_b[1][0]
92
+
93
+ # @type [Integer]
94
+ weight1 = slug_a.count("/") <=> slug_b.count("/")
95
+ if weight1.zero?
96
+ slug_b.count("/:") <=> slug_a.count("/:")
97
+ else
98
+ weight1
83
99
  end
100
+ end.reverse!
101
+ end
84
102
 
85
- [file_slug, segment_keys]
86
- end
103
+ def locale_for(slug)
104
+ possible_locale_segment = slug.split("/").first.to_sym
87
105
 
88
- def generate_localized_file_slugs(site, file_slug)
89
- site.config.available_locales.map do |locale|
90
- if locale == site.config.default_locale && !site.config.prefix_default_locale
91
- file_slug
92
- else
93
- "#{locale}/#{file_slug}"
94
- end
95
- end
106
+ if site.config.available_locales.include? possible_locale_segment
107
+ possible_locale_segment
108
+ else
109
+ site.config.default_locale
96
110
  end
97
111
  end
98
112
  end
@@ -1,55 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "bridgetown-core/rack/routes"
4
-
5
3
  module Bridgetown
6
4
  module Routes
7
5
  class ManifestRouter < Bridgetown::Rack::Routes
8
6
  priority :lowest
9
7
 
10
- route do |r|
11
- unless bridgetown_site
12
- Bridgetown.logger.warn(
13
- "The `bridgetown_routes` plugin hasn't been configured in the Roda app."
14
- )
15
- return
16
- end
17
-
18
- Bridgetown::Routes::Manifest.generate_manifest(bridgetown_site).each do |route|
19
- file, localized_file_slugs, segment_keys = route
20
-
21
- localized_file_slugs.each do |file_slug|
22
- add_route(r, file, file_slug, segment_keys)
23
- end
24
- end
25
-
26
- nil
27
- end
28
-
29
- private
30
-
31
- def add_route(route, file, file_slug, segment_keys)
32
- route.on file_slug do |*segment_values|
33
- response["X-Bridgetown-SSR"] = "1"
34
- # eval_route_file caches when Bridgetown.env.production?
35
- Bridgetown::Routes::CodeBlocks.eval_route_file file, file_slug, @_roda_app
36
-
37
- segment_values.each_with_index do |value, index|
38
- route.params[segment_keys[index]] ||= value
39
- end
40
-
41
- # set route locale
42
- locale = Bridgetown::Routes::Manifest.locale_for(file_slug, bridgetown_site)
43
- I18n.locale = locale
44
- route.params[:locale] = locale
45
-
46
- route_block = Bridgetown::Routes::CodeBlocks.route_block(file_slug)
47
- response.instance_variable_set(
48
- :@_route_file_code, route_block.instance_variable_get(:@_route_file_code)
49
- ) # could be nil
50
- @_roda_app.instance_exec(route, &route_block)
51
- end
52
- end
8
+ route(&:file_routes)
53
9
  end
54
10
  end
55
11
  end
@@ -11,13 +11,20 @@ module Bridgetown
11
11
  end
12
12
 
13
13
  # @param config [Bridgetown::Configuration::ConfigurationDSL]
14
- Bridgetown.initializer :"bridgetown-routes" do |config|
14
+ Bridgetown.initializer :"bridgetown-routes" do |
15
+ config,
16
+ additional_source_paths: [],
17
+ additional_extensions: []
18
+ |
15
19
  config.init :ssr # ensure we already have touchdown!
16
20
 
17
21
  config.routes ||= {}
18
- config.routes.source_paths ||= ["_routes", "#{config.islands_dir}/routes"]
22
+ config.routes.source_paths ||= ["_routes"]
19
23
  config.routes.extensions ||= %w(rb md serb erb liquid)
20
24
 
25
+ config.routes.source_paths += Array(additional_source_paths)
26
+ config.routes.extensions += Array(additional_extensions)
27
+
21
28
  config.only :server do
22
29
  require_relative "bridgetown-routes/manifest_router"
23
30
  end
@@ -25,14 +25,67 @@ class Roda
25
25
  end
26
26
 
27
27
  def self.configure(app, _opts = {})
28
- return unless app.opts[:bridgetown_site].nil?
28
+ app.root_hook do
29
+ routes_dir = File.expand_path(
30
+ bridgetown_site.config.routes.source_paths.first,
31
+ bridgetown_site.config.source
32
+ )
33
+ file = routes_manifest.glob_routes(routes_dir, "index").first
34
+ next unless file
35
+
36
+ run_file_route(file, slug: "index")
37
+ end
38
+
39
+ if app.opts[:bridgetown_site]
40
+ app.opts[:routes_manifest] ||=
41
+ Bridgetown::Routes::Manifest.new(app.opts[:bridgetown_site])
42
+ return
43
+ end
29
44
 
30
45
  raise "Roda app failure: the bridgetown_ssr plugin must be registered before " \
31
46
  "bridgetown_routes"
32
47
  end
33
48
 
34
49
  module InstanceMethods
35
- def render_with(data: {}) # rubocop:todo Metrics/AbcSize, Metrics/MethodLength
50
+ def routes_manifest
51
+ self.class.opts[:routes_manifest]
52
+ end
53
+
54
+ def run_file_route(file, slug:)
55
+ response["X-Bridgetown-Routes"] = "1"
56
+ # eval_route_file caches when Bridgetown.env.production?
57
+ Bridgetown::Routes::CodeBlocks.eval_route_file file, slug, self
58
+
59
+ # set route locale
60
+ locale = routes_manifest.locale_for(slug)
61
+ I18n.locale = request.params[:locale] = locale
62
+
63
+ # get the route block extracted from the file at slug
64
+ route_block = Bridgetown::Routes::CodeBlocks.route_block(slug)
65
+ response.instance_variable_set(
66
+ :@_route_file_code, route_block.instance_variable_get(:@_route_file_code)
67
+ ) # could be nil
68
+ response.instance_variable_set(
69
+ :@_front_matter_line_count,
70
+ route_block.instance_variable_get(:@_front_matter_line_count)
71
+ ) # could be nil
72
+ instance_exec(request, &route_block)
73
+ end
74
+
75
+ def front_matter(&block)
76
+ b = block.binding
77
+ denylisted = %i(r argv)
78
+ data = b.local_variables.filter_map do |key|
79
+ next if denylisted.any? key
80
+
81
+ [key, b.local_variable_get(key)]
82
+ end.to_h
83
+
84
+ Bridgetown::FrontMatter::RubyFrontMatter.new(data:).tap { _1.instance_exec(&block) }.to_h
85
+ end
86
+
87
+ def render_with(data: {}, &) # rubocop:todo Metrics/AbcSize, Metrics/MethodLength
88
+ data = front_matter(&) if data.empty? && block_given?
36
89
  path = Kernel.caller_locations(1, 1).first.path
37
90
  source_path = Pathname.new(path).relative_path_from(
38
91
  bridgetown_site.in_source_dir("_routes")
@@ -50,14 +103,16 @@ class Roda
50
103
  )
51
104
  ).read do
52
105
  data[:_collection_] = bridgetown_site.collections.pages
106
+ data[:_original_path_] = path
53
107
  data[:_relative_path_] = source_path
108
+ data[:_front_matter_line_count_] = response._front_matter_line_count
54
109
  data[:_content_] = code
55
110
  data
56
111
  end
57
112
 
58
113
  Bridgetown::Model::Base.new(data).to_resource.tap do |resource|
59
114
  resource.roda_app = self
60
- end.read!.transform!.output
115
+ end.read!
61
116
  end
62
117
 
63
118
  def render(...)
@@ -65,17 +120,45 @@ class Roda
65
120
  end
66
121
 
67
122
  def view(view_class: Bridgetown::ERBView)
123
+ # TODO: support user choosing templates by extension rather than class
68
124
  response._fake_resource_view(
69
- view_class: view_class, roda_app: self, bridgetown_site: bridgetown_site
125
+ view_class:, roda_app: self, bridgetown_site:
70
126
  )
71
127
  end
72
128
  end
73
129
 
130
+ module RequestMethods
131
+ # This runs through all of the routes in the manifest, setting up Roda blocks for execution
132
+ def file_routes
133
+ scope.routes_manifest.routes.each do |route|
134
+ file, localized_file_slugs, segment_keys = route
135
+
136
+ localized_file_slugs.each do |slug|
137
+ # This sets up an initial Roda route block at the slug, and handles segments as params
138
+ #
139
+ # _routes/nested/[slug].erb -> "nested/:slug"
140
+ # "nested/123" -> r.params[:slug] == 123
141
+ on slug do |*segment_values|
142
+ segment_values.each_with_index do |value, index|
143
+ params[segment_keys[index]] ||= value
144
+ end
145
+
146
+ # This is provided as an instance method by our Roda plugin:
147
+ scope.run_file_route(file, slug:)
148
+ end
149
+ end
150
+ end
151
+
152
+ nil # be sure not to return the above array loop
153
+ end
154
+ end
155
+
74
156
  module ResponseMethods
75
157
  # template string provided, if available, by the saved code block
76
- def _route_file_code
77
- @_route_file_code
78
- end
158
+ def _route_file_code = @_route_file_code
159
+
160
+ # we need to know where the real template starts for good error reporting
161
+ def _front_matter_line_count = @_front_matter_line_count
79
162
 
80
163
  def _fake_resource_view(view_class:, roda_app:, bridgetown_site:)
81
164
  @_fake_resource_views ||= {}
@@ -83,7 +166,7 @@ class Roda
83
166
  # TODO: use a Stuct for better performance...?
84
167
  HashWithDotAccess::Hash.new({
85
168
  data: {},
86
- roda_app: roda_app,
169
+ roda_app:,
87
170
  site: bridgetown_site,
88
171
  })
89
172
  )
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bridgetown-routes
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.4
4
+ version: 2.0.0.beta1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bridgetown Team
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-03-28 00:00:00.000000000 Z
11
+ date: 2024-08-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bridgetown-core
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - '='
18
18
  - !ruby/object:Gem::Version
19
- version: 1.3.4
19
+ version: 2.0.0.beta1
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: 1.3.4
26
+ version: 2.0.0.beta1
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: roda-route_list
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -73,11 +73,11 @@ required_ruby_version: !ruby/object:Gem::Requirement
73
73
  version: '0'
74
74
  required_rubygems_version: !ruby/object:Gem::Requirement
75
75
  requirements:
76
- - - ">="
76
+ - - ">"
77
77
  - !ruby/object:Gem::Version
78
- version: '0'
78
+ version: 1.3.1
79
79
  requirements: []
80
- rubygems_version: 3.2.33
80
+ rubygems_version: 3.3.26
81
81
  signing_key:
82
82
  specification_version: 4
83
83
  summary: A Bridgetown plugin to add support for file-based Roda backend routes within