mayu-live 0.0.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 (204) hide show
  1. checksums.yaml +7 -0
  2. data/COPYING +661 -0
  3. data/README.md +598 -0
  4. data/exe/mayu +33 -0
  5. data/lib/mayu/app_metrics.rb +93 -0
  6. data/lib/mayu/banner.rb +12 -0
  7. data/lib/mayu/client/README.md +17 -0
  8. data/lib/mayu/client/dist/DecompressionStreamPolyfill-3ceba43e.js +1 -0
  9. data/lib/mayu/client/dist/DecompressionStreamPolyfill-3ceba43e.js.br +0 -0
  10. data/lib/mayu/client/dist/DecompressionStreamPolyfill-3ceba43e.js.map +1 -0
  11. data/lib/mayu/client/dist/DecompressionStreamPolyfill-3ceba43e.js.map.br +0 -0
  12. data/lib/mayu/client/dist/custom-elements/mayu-alert-cd7ad2a4.js +1 -0
  13. data/lib/mayu/client/dist/custom-elements/mayu-alert-cd7ad2a4.js.map +1 -0
  14. data/lib/mayu/client/dist/custom-elements/mayu-disconnected-9f349f46.js +1 -0
  15. data/lib/mayu/client/dist/custom-elements/mayu-disconnected-9f349f46.js.map +1 -0
  16. data/lib/mayu/client/dist/custom-elements/mayu-exception-63df4e8c.js +1 -0
  17. data/lib/mayu/client/dist/custom-elements/mayu-exception-63df4e8c.js.map +1 -0
  18. data/lib/mayu/client/dist/custom-elements/mayu-ping-c498c2a6.js +1 -0
  19. data/lib/mayu/client/dist/custom-elements/mayu-ping-c498c2a6.js.map +1 -0
  20. data/lib/mayu/client/dist/custom-elements/mayu-progress-bar-eb3e1ac8.js +1 -0
  21. data/lib/mayu/client/dist/custom-elements/mayu-progress-bar-eb3e1ac8.js.map +1 -0
  22. data/lib/mayu/client/dist/entries.json +3 -0
  23. data/lib/mayu/client/dist/main-4b49dbc4.js +1 -0
  24. data/lib/mayu/client/dist/main-4b49dbc4.js.br +0 -0
  25. data/lib/mayu/client/dist/main-4b49dbc4.js.map +1 -0
  26. data/lib/mayu/client/dist/main-4b49dbc4.js.map.br +0 -0
  27. data/lib/mayu/client/package.json +39 -0
  28. data/lib/mayu/client/rollup.config.js +81 -0
  29. data/lib/mayu/client/src/DecompressionStream.ts +15 -0
  30. data/lib/mayu/client/src/DecompressionStreamPolyfill.ts +43 -0
  31. data/lib/mayu/client/src/MimeTypes.ts +4 -0
  32. data/lib/mayu/client/src/NodeTree.ts +445 -0
  33. data/lib/mayu/client/src/custom-elements/mayu-alert.html +137 -0
  34. data/lib/mayu/client/src/custom-elements/mayu-alert.ts +62 -0
  35. data/lib/mayu/client/src/custom-elements/mayu-disconnected.html +134 -0
  36. data/lib/mayu/client/src/custom-elements/mayu-disconnected.ts +51 -0
  37. data/lib/mayu/client/src/custom-elements/mayu-exception.html +79 -0
  38. data/lib/mayu/client/src/custom-elements/mayu-exception.ts +28 -0
  39. data/lib/mayu/client/src/custom-elements/mayu-log.html +70 -0
  40. data/lib/mayu/client/src/custom-elements/mayu-log.ts +42 -0
  41. data/lib/mayu/client/src/custom-elements/mayu-ping.html +36 -0
  42. data/lib/mayu/client/src/custom-elements/mayu-ping.ts +53 -0
  43. data/lib/mayu/client/src/custom-elements/mayu-progress-bar.html +44 -0
  44. data/lib/mayu/client/src/custom-elements/mayu-progress-bar.ts +40 -0
  45. data/lib/mayu/client/src/custom-elements/types.d.ts +4 -0
  46. data/lib/mayu/client/src/global.d.ts +26 -0
  47. data/lib/mayu/client/src/h.ts +27 -0
  48. data/lib/mayu/client/src/logger.ts +56 -0
  49. data/lib/mayu/client/src/main.ts +271 -0
  50. data/lib/mayu/client/src/serializeEvent.ts +90 -0
  51. data/lib/mayu/client/src/stream.ts +175 -0
  52. data/lib/mayu/client/src/types.ts +1 -0
  53. data/lib/mayu/client/src/utils.ts +71 -0
  54. data/lib/mayu/client/tsconfig.json +18 -0
  55. data/lib/mayu/colors.rb +34 -0
  56. data/lib/mayu/commands/base.rb +22 -0
  57. data/lib/mayu/commands/build.rb +82 -0
  58. data/lib/mayu/commands.rb +53 -0
  59. data/lib/mayu/component/base.rb +177 -0
  60. data/lib/mayu/component/handler_ref.rb +99 -0
  61. data/lib/mayu/component/helpers.rb +93 -0
  62. data/lib/mayu/component/interface.rb +18 -0
  63. data/lib/mayu/component/wrapper.rb +165 -0
  64. data/lib/mayu/component.rb +54 -0
  65. data/lib/mayu/configuration.rb +195 -0
  66. data/lib/mayu/disable_sorbet.rb +23 -0
  67. data/lib/mayu/environment.rb +151 -0
  68. data/lib/mayu/event_stream.rb +158 -0
  69. data/lib/mayu/fetch.rb +88 -0
  70. data/lib/mayu/html.rb +53 -0
  71. data/lib/mayu/html.yaml +767 -0
  72. data/lib/mayu/message_cipher.rb +172 -0
  73. data/lib/mayu/message_cipher.test.rb +16 -0
  74. data/lib/mayu/metrics/collector.rb +161 -0
  75. data/lib/mayu/metrics/exporter.rb +47 -0
  76. data/lib/mayu/metrics/reporter.rb +187 -0
  77. data/lib/mayu/metrics.rb +82 -0
  78. data/lib/mayu/ref_counter.rb +57 -0
  79. data/lib/mayu/resources/README.md +14 -0
  80. data/lib/mayu/resources/asset.rb +71 -0
  81. data/lib/mayu/resources/assets.rb +76 -0
  82. data/lib/mayu/resources/dependency_graph.rb +306 -0
  83. data/lib/mayu/resources/dot_exporter.rb +167 -0
  84. data/lib/mayu/resources/generators/base.rb +18 -0
  85. data/lib/mayu/resources/generators/copy_file.rb +26 -0
  86. data/lib/mayu/resources/generators/image.rb +106 -0
  87. data/lib/mayu/resources/generators/write_file.rb +39 -0
  88. data/lib/mayu/resources/hot_swap/file_watcher.rb +69 -0
  89. data/lib/mayu/resources/hot_swap.rb +46 -0
  90. data/lib/mayu/resources/mermaid_exporter.rb +210 -0
  91. data/lib/mayu/resources/registry.rb +190 -0
  92. data/lib/mayu/resources/resolver/base.rb +32 -0
  93. data/lib/mayu/resources/resolver/filesystem.rb +94 -0
  94. data/lib/mayu/resources/resolver/static.rb +27 -0
  95. data/lib/mayu/resources/resolver.rb +13 -0
  96. data/lib/mayu/resources/resource.rb +150 -0
  97. data/lib/mayu/resources/transformers/__test__/css/adjacent_selectors.in.css +3 -0
  98. data/lib/mayu/resources/transformers/__test__/css/adjacent_selectors.out.css +6 -0
  99. data/lib/mayu/resources/transformers/__test__/css/attributes.in.css +3 -0
  100. data/lib/mayu/resources/transformers/__test__/css/attributes.out.css +6 -0
  101. data/lib/mayu/resources/transformers/__test__/css/composes.in.css +6 -0
  102. data/lib/mayu/resources/transformers/__test__/css/composes.out.css +10 -0
  103. data/lib/mayu/resources/transformers/__test__/css/element_selectors.in.css +3 -0
  104. data/lib/mayu/resources/transformers/__test__/css/element_selectors.out.css +6 -0
  105. data/lib/mayu/resources/transformers/__test__/css/has.in.css +7 -0
  106. data/lib/mayu/resources/transformers/__test__/css/has.out.css +10 -0
  107. data/lib/mayu/resources/transformers/__test__/css/media_queries.in.css +8 -0
  108. data/lib/mayu/resources/transformers/__test__/css/media_queries.out.css +12 -0
  109. data/lib/mayu/resources/transformers/__test__/css/pseudo_classes.in.css +5 -0
  110. data/lib/mayu/resources/transformers/__test__/css/pseudo_classes.out.css +6 -0
  111. data/lib/mayu/resources/transformers/__test__/haml/README.md +10 -0
  112. data/lib/mayu/resources/transformers/__test__/haml/case.haml +8 -0
  113. data/lib/mayu/resources/transformers/__test__/haml/case.rb +15 -0
  114. data/lib/mayu/resources/transformers/__test__/haml/class_names.haml +13 -0
  115. data/lib/mayu/resources/transformers/__test__/haml/class_names.rb +26 -0
  116. data/lib/mayu/resources/transformers/__test__/haml/comments.haml +5 -0
  117. data/lib/mayu/resources/transformers/__test__/haml/comments.rb +5 -0
  118. data/lib/mayu/resources/transformers/__test__/haml/css.haml +3 -0
  119. data/lib/mayu/resources/transformers/__test__/haml/css.rb +11 -0
  120. data/lib/mayu/resources/transformers/__test__/haml/dashes.haml +3 -0
  121. data/lib/mayu/resources/transformers/__test__/haml/dashes.rb +11 -0
  122. data/lib/mayu/resources/transformers/__test__/haml/early_return.haml +4 -0
  123. data/lib/mayu/resources/transformers/__test__/haml/early_return.rb +9 -0
  124. data/lib/mayu/resources/transformers/__test__/haml/early_return2.haml +3 -0
  125. data/lib/mayu/resources/transformers/__test__/haml/early_return2.rb +6 -0
  126. data/lib/mayu/resources/transformers/__test__/haml/handlers.haml +6 -0
  127. data/lib/mayu/resources/transformers/__test__/haml/handlers.rb +12 -0
  128. data/lib/mayu/resources/transformers/__test__/haml/if_else.haml +6 -0
  129. data/lib/mayu/resources/transformers/__test__/haml/if_else.rb +12 -0
  130. data/lib/mayu/resources/transformers/__test__/haml/interpolation.haml +8 -0
  131. data/lib/mayu/resources/transformers/__test__/haml/interpolation.rb +11 -0
  132. data/lib/mayu/resources/transformers/__test__/haml/object_ref_as_key.haml +1 -0
  133. data/lib/mayu/resources/transformers/__test__/haml/object_ref_as_key.rb +5 -0
  134. data/lib/mayu/resources/transformers/__test__/haml/props.haml +4 -0
  135. data/lib/mayu/resources/transformers/__test__/haml/props.rb +11 -0
  136. data/lib/mayu/resources/transformers/__test__/haml/slots.haml +5 -0
  137. data/lib/mayu/resources/transformers/__test__/haml/slots.rb +9 -0
  138. data/lib/mayu/resources/transformers/__test__/haml/slots_dynamic.haml +3 -0
  139. data/lib/mayu/resources/transformers/__test__/haml/slots_dynamic.rb +9 -0
  140. data/lib/mayu/resources/transformers/__test__/haml/slots_fallback.haml +3 -0
  141. data/lib/mayu/resources/transformers/__test__/haml/slots_fallback.rb +5 -0
  142. data/lib/mayu/resources/transformers/__test__/haml/spacing.haml +5 -0
  143. data/lib/mayu/resources/transformers/__test__/haml/spacing.rb +14 -0
  144. data/lib/mayu/resources/transformers/__test__/haml/spacing2.haml +10 -0
  145. data/lib/mayu/resources/transformers/__test__/haml/spacing2.rb +11 -0
  146. data/lib/mayu/resources/transformers/__test__/haml/spacing3.haml +3 -0
  147. data/lib/mayu/resources/transformers/__test__/haml/spacing3.rb +10 -0
  148. data/lib/mayu/resources/transformers/css/rouge_lexer.rb +841 -0
  149. data/lib/mayu/resources/transformers/css.rb +100 -0
  150. data/lib/mayu/resources/transformers/css.test.rb +87 -0
  151. data/lib/mayu/resources/transformers/haml.rb +984 -0
  152. data/lib/mayu/resources/transformers/haml.test.rb +114 -0
  153. data/lib/mayu/resources/types/README.md +36 -0
  154. data/lib/mayu/resources/types/base.rb +35 -0
  155. data/lib/mayu/resources/types/component.rb +198 -0
  156. data/lib/mayu/resources/types/image.rb +169 -0
  157. data/lib/mayu/resources/types/javascript.rb +50 -0
  158. data/lib/mayu/resources/types/nil.rb +23 -0
  159. data/lib/mayu/resources/types/stylesheet.rb +119 -0
  160. data/lib/mayu/resources/types/svg.rb +69 -0
  161. data/lib/mayu/resources/types.rb +37 -0
  162. data/lib/mayu/routes.rb +170 -0
  163. data/lib/mayu/routing/builder.rb +108 -0
  164. data/lib/mayu/routing/matcher.rb +58 -0
  165. data/lib/mayu/routing/routes.rb +85 -0
  166. data/lib/mayu/routing.rb +17 -0
  167. data/lib/mayu/server/app.rb +494 -0
  168. data/lib/mayu/server/controller.rb +152 -0
  169. data/lib/mayu/server/errors.rb +110 -0
  170. data/lib/mayu/server/file_server.rb +140 -0
  171. data/lib/mayu/server.rb +63 -0
  172. data/lib/mayu/session.rb +358 -0
  173. data/lib/mayu/state/README.md +6 -0
  174. data/lib/mayu/state/action_creator.rb +191 -0
  175. data/lib/mayu/state/action_wrapper.rb +30 -0
  176. data/lib/mayu/state/loader.rb +220 -0
  177. data/lib/mayu/state/store.rb +82 -0
  178. data/lib/mayu/state.rb +8 -0
  179. data/lib/mayu/state.test.rb +97 -0
  180. data/lib/mayu/utils.rb +114 -0
  181. data/lib/mayu/vdom/children.rb +117 -0
  182. data/lib/mayu/vdom/component_marshaler.rb +53 -0
  183. data/lib/mayu/vdom/css_attributes.rb +131 -0
  184. data/lib/mayu/vdom/descriptor.rb +151 -0
  185. data/lib/mayu/vdom/descriptor.test.rb +26 -0
  186. data/lib/mayu/vdom/dom.rb +239 -0
  187. data/lib/mayu/vdom/h.rb +22 -0
  188. data/lib/mayu/vdom/id_generator.rb +55 -0
  189. data/lib/mayu/vdom/interfaces.rb +186 -0
  190. data/lib/mayu/vdom/marshalling.rb +78 -0
  191. data/lib/mayu/vdom/reconciliation.rb +205 -0
  192. data/lib/mayu/vdom/reconciliation.test.rb +56 -0
  193. data/lib/mayu/vdom/special_elements.rb +108 -0
  194. data/lib/mayu/vdom/update_context.rb +180 -0
  195. data/lib/mayu/vdom/vdom.perf.test.rb +146 -0
  196. data/lib/mayu/vdom/vnode.rb +266 -0
  197. data/lib/mayu/vdom/vtree.rb +672 -0
  198. data/lib/mayu/vdom/vtree.test.rb +68 -0
  199. data/lib/mayu/vdom.rb +8 -0
  200. data/lib/mayu/vdom.test.rb +73 -0
  201. data/lib/mayu/version.rb +6 -0
  202. data/lib/mayu.rb +8 -0
  203. data/mayu-live.gemspec +70 -0
  204. metadata +612 -0
@@ -0,0 +1,119 @@
1
+ # typed: strict
2
+
3
+ require "brotli"
4
+ require_relative "../transformers/css"
5
+
6
+ module Mayu
7
+ module Resources
8
+ module Types
9
+ class Stylesheet < Base
10
+ Classes = T.type_alias { T::Hash[Symbol, String] }
11
+
12
+ class ClassNames
13
+ extend T::Sig
14
+
15
+ sig { params(classes: Classes).void }
16
+ def initialize(classes)
17
+ @classes = classes
18
+ end
19
+
20
+ sig { params(ident: Symbol).returns(String) }
21
+ def method_missing(ident)
22
+ @classes[ident].to_s
23
+ end
24
+
25
+ sig { params(args: T.untyped).returns(String) }
26
+ def [](*args)
27
+ args
28
+ .each_with_object(Set.new) { |arg, set| add_to_result(set, arg) }
29
+ .join(" ")
30
+ end
31
+
32
+ private
33
+
34
+ sig { params(result: T::Set[String], arg: T.untyped).void }
35
+ def add_to_result(result, arg)
36
+ case arg
37
+ when Symbol
38
+ if klass = @classes[arg]
39
+ result.add(klass)
40
+ end
41
+ when String
42
+ result.add(arg)
43
+ when Array
44
+ arg.each { add_to_result(result, _1) }
45
+ when Hash
46
+ arg.each { add_to_result(result, _1) if _2 }
47
+ end
48
+ end
49
+ end
50
+
51
+ extend T::Sig
52
+
53
+ sig { returns(Classes) }
54
+ attr_reader :classes
55
+
56
+ sig { params(resource: Resource).void }
57
+ def initialize(resource)
58
+ super
59
+ klasses = {}
60
+
61
+ transform_result =
62
+ Transformers::CSS.transform(
63
+ source: resource.read(encoding: "utf-8"),
64
+ source_path: resource.path
65
+ )
66
+
67
+ transform_result.classes
68
+
69
+ @source = T.let(transform_result.output, String)
70
+ @content_hash = T.let(transform_result.content_hash, String)
71
+ @classes = T.let(transform_result.classes, Classes)
72
+ @filename = T.let(transform_result.filename, String)
73
+ @source_map =
74
+ T.let(transform_result.source_map, T::Hash[String, T.untyped])
75
+ @classnames = T.let(nil, T.nilable(ClassNames))
76
+ end
77
+
78
+ sig { returns(ClassNames) }
79
+ def classnames
80
+ @classnames ||= ClassNames.new(self.classes)
81
+ end
82
+
83
+ sig { returns(T::Array[Asset]) }
84
+ def assets
85
+ source_map_link = "\n/*# sourceMappingURL=#{@filename}.map */\n"
86
+
87
+ [
88
+ Asset.new(
89
+ @filename,
90
+ Generators::WriteFile.new(
91
+ contents: @source + source_map_link,
92
+ compress: true
93
+ )
94
+ ),
95
+ Asset.new(
96
+ @filename + ".map",
97
+ Generators::WriteFile.new(
98
+ contents: JSON.generate(@source_map),
99
+ compress: true
100
+ )
101
+ )
102
+ ]
103
+ end
104
+
105
+ MarshalFormat = T.type_alias { [Classes, String, String, String] }
106
+
107
+ sig { returns(MarshalFormat) }
108
+ def marshal_dump
109
+ [@classes, @source, @content_hash, @filename]
110
+ end
111
+
112
+ sig { params(args: MarshalFormat).void }
113
+ def marshal_load(args)
114
+ @classes, @source, @content_hash, @filename = args
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ require "svg_optimizer"
5
+ require_relative "base"
6
+
7
+ module Mayu
8
+ module Resources
9
+ module Types
10
+ class SVG < Base
11
+ extend T::Sig
12
+
13
+ SVG_OPTIMIZER_PLUGINS =
14
+ T.let(
15
+ SvgOptimizer::DEFAULT_PLUGINS -
16
+ [
17
+ # The following plugin sets fill="none" in some cases
18
+ # which breaks the fontawesome icons... That's why
19
+ # it's disabled...
20
+ SvgOptimizer::Plugins::RemoveUselessStrokeAndFill
21
+ ],
22
+ T::Array[T.class_of(SvgOptimizer::Plugins::Base)]
23
+ )
24
+
25
+ sig { params(resource: Resource).void }
26
+ def initialize(resource)
27
+ @resource = resource
28
+
29
+ original = resource.read(encoding: "utf-8")
30
+ optimized = SvgOptimizer.optimize(original, SVG_OPTIMIZER_PLUGINS)
31
+
32
+ content_hash =
33
+ Base64.urlsafe_encode64(Digest::SHA256.digest(optimized))
34
+
35
+ @filename = T.let("#{content_hash}.svg", String)
36
+ @source = T.let("#{optimized}\n", String)
37
+ end
38
+
39
+ sig { returns(T::Array[Asset]) }
40
+ def assets
41
+ [
42
+ Asset.new(
43
+ @filename,
44
+ Generators::WriteFile.new(contents: @source, compress: true)
45
+ )
46
+ ]
47
+ end
48
+
49
+ sig { returns(String) }
50
+ def to_s = src
51
+
52
+ sig { returns(String) }
53
+ def src = "/__mayu/static/#{@filename}"
54
+
55
+ MarshalFormat = T.type_alias { [String, String] }
56
+
57
+ sig { returns(MarshalFormat) }
58
+ def marshal_dump
59
+ [@source, @filename]
60
+ end
61
+
62
+ sig { params(args: MarshalFormat).void }
63
+ def marshal_load(args)
64
+ @source, @filename = args
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ require_relative "types/nil"
5
+ require_relative "types/component"
6
+ require_relative "types/image"
7
+ require_relative "types/stylesheet"
8
+ require_relative "types/javascript"
9
+ require_relative "types/svg"
10
+
11
+ module Mayu
12
+ module Resources
13
+ module Types
14
+ extend T::Sig
15
+
16
+ sig { params(path: String).returns(T.class_of(Types::Base)) }
17
+ def self.for_path(path)
18
+ case path
19
+ when /\.rb\z/
20
+ return Component
21
+ when /\.haml\z/
22
+ return Component
23
+ when /\.js\z/
24
+ return JavaScript
25
+ when /\.css\z/
26
+ return Stylesheet
27
+ when /\.(png|jpe?g|gif|webp)$\z/
28
+ return Image
29
+ when /\.svg\z/
30
+ return SVG
31
+ end
32
+
33
+ raise "No type for #{path}"
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,170 @@
1
+ # typed: strict
2
+
3
+ require "terminal-table"
4
+
5
+ module Mayu
6
+ module Routes
7
+ extend T::Sig
8
+
9
+ class NotFoundError < StandardError
10
+ end
11
+
12
+ class Route < T::Struct
13
+ const :path, String
14
+ const :regexp, Regexp
15
+ const :layouts, T::Array[String]
16
+ const :template, String
17
+ end
18
+
19
+ class RouteMatch < T::Struct
20
+ const :params, T::Hash[Symbol, String]
21
+ const :layouts, T::Array[String]
22
+ const :template, String
23
+ end
24
+
25
+ EXTENSIONS = T.let(%w[.rb .haml].freeze, T::Array[String])
26
+
27
+ PAGE_FILENAME = "page"
28
+ LAYOUT_FILENAME = "layout"
29
+ NOT_FOUND_FILENAME = "404"
30
+
31
+ sig do
32
+ params(
33
+ root: String,
34
+ routes: T::Array[Route],
35
+ layouts: T::Array[String],
36
+ path: T::Array[String],
37
+ level: Integer
38
+ ).returns(T::Array[Route])
39
+ end
40
+ def self.build_routes(root, routes: [], layouts: [], path: [], level: 0)
41
+ dir = T.unsafe(File).join(root, *path)
42
+ return routes unless File.directory?(dir)
43
+
44
+ entries = Dir.entries(dir) - %w[. ..]
45
+
46
+ if layout = find_and_delete(entries, LAYOUT_FILENAME)
47
+ layouts += [T.unsafe(File).join(*path, layout)]
48
+ end
49
+
50
+ if page = find_and_delete(entries, PAGE_FILENAME)
51
+ routes.push(
52
+ Route.new(
53
+ path: path.join("/"),
54
+ regexp: path_to_regexp(path.join("/")),
55
+ layouts:,
56
+ template: T.unsafe(File).join(*path, page)
57
+ )
58
+ )
59
+ end
60
+
61
+ entries.each do |entry|
62
+ build_routes(
63
+ File.join(root),
64
+ routes:,
65
+ layouts:,
66
+ path: path + [entry],
67
+ level: level.succ
68
+ )
69
+ end
70
+
71
+ if not_found = find_and_delete(entries, NOT_FOUND_FILENAME)
72
+ routes.push(
73
+ Route.new(
74
+ path: path.join("/"),
75
+ regexp: path_to_regexp([*path, "*"].join("/")),
76
+ layouts:,
77
+ template: T.unsafe(File).join(*path, not_found)
78
+ )
79
+ )
80
+ else
81
+ Console.logger.warn(self) { <<~EOF } if level.zero?
82
+ There is no #{NOT_FOUND_FILENAME} in the app root,
83
+ you should probably create one.
84
+ EOF
85
+ end
86
+
87
+ routes
88
+ end
89
+
90
+ sig { params(routes: T::Array[Route]).void }
91
+ def self.log_routes(routes)
92
+ Console
93
+ .logger
94
+ .info(self) do
95
+ Terminal::Table.new do |t|
96
+ t.headings =
97
+ %w[Path Template Layouts Regexp].map { "\e[1m#{_1}\e[0m" }
98
+ t.style = { all_separators: true, border: :unicode }
99
+
100
+ routes.each do |route|
101
+ t.add_row(
102
+ [
103
+ "/#{route.path}",
104
+ route.template,
105
+ route.layouts.join("\n"),
106
+ "/#{route.regexp.to_s}/"
107
+ ]
108
+ )
109
+ end
110
+ end
111
+ end
112
+ end
113
+
114
+ sig do
115
+ params(routes: T::Array[Route], request_path: String).returns(RouteMatch)
116
+ end
117
+ def self.match_route(routes, request_path)
118
+ routes.each do |route|
119
+ match = route.regexp.match(request_path)
120
+
121
+ next unless match
122
+
123
+ return(
124
+ RouteMatch.new(
125
+ template: route.template,
126
+ layouts: route.layouts,
127
+ params:
128
+ match
129
+ .named_captures
130
+ .transform_keys(&:to_sym)
131
+ .transform_values(&:to_s)
132
+ )
133
+ )
134
+ end
135
+
136
+ raise NotFoundError,
137
+ "Page not found, and no 404 page either. You should probably create one."
138
+ end
139
+
140
+ sig do
141
+ params(path: String, formats: T::Hash[Symbol, Regexp]).returns(Regexp)
142
+ end
143
+ def self.path_to_regexp(path, formats: {})
144
+ parts =
145
+ path
146
+ .delete_prefix("/")
147
+ .split("/")
148
+ .map do |part|
149
+ case part
150
+ when "*"
151
+ ".+"
152
+ when /\A:(?<var>\w+)\Z/
153
+ var = Regexp.escape($~[:var])
154
+ "(?<#{var}>[^/]+)"
155
+ else
156
+ Regexp.escape(part).to_s
157
+ end
158
+ end
159
+
160
+ Regexp.new('\A\/' + parts.join('\/') + '\Z')
161
+ end
162
+
163
+ sig { params(a: T::Array[String], name: String).returns(T.nilable(String)) }
164
+ def self.find_and_delete(a, name)
165
+ EXTENSIONS.find do |extension|
166
+ a.delete("#{name}#{extension}")&.tap { return _1 }
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,108 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Mayu
5
+ module Routing
6
+ class Builder
7
+ extend T::Sig
8
+
9
+ IGNORE = T.let(%w[. ..].freeze, T::Array[String])
10
+ EXTENSIONS = T.let(%w[.rb .haml].freeze, T::Array[String])
11
+
12
+ sig { params(root: String).returns(Routes::Route) }
13
+ def self.build(root)
14
+ new(root).build
15
+ end
16
+
17
+ sig { params(root: String).void }
18
+ def initialize(root)
19
+ @root = T.let(File.expand_path(root), String)
20
+ end
21
+
22
+ sig { returns(Routes::Route) }
23
+ def build
24
+ traverse_directory(Routes::Route.new, path: [])
25
+ end
26
+
27
+ private
28
+
29
+ sig do
30
+ params(route: Routes::Route, path: T::Array[String]).returns(
31
+ Routes::Route
32
+ )
33
+ end
34
+ def visit(route, path: [])
35
+ absolute_path = File.join(@root, path)
36
+ basename = File.basename(absolute_path)
37
+ stat = File.stat(absolute_path)
38
+
39
+ case
40
+ when stat.directory?
41
+ visit_dir(route, path:)
42
+ when stat.file?
43
+ visit_file(route, path:)
44
+ end
45
+
46
+ route
47
+ end
48
+
49
+ sig do
50
+ params(route: Routes::Route, path: T::Array[String]).returns(
51
+ Routes::Route
52
+ )
53
+ end
54
+ def visit_dir(route, path: [])
55
+ if match = path.last.to_s.match(/\A:(\w+)\z/)
56
+ route.add_route(
57
+ traverse_directory(Routes::Param.new(match[1].to_s), path:)
58
+ )
59
+ else
60
+ route.add_route(
61
+ traverse_directory(Routes::Named.new(path.last.to_s), path:)
62
+ )
63
+ end
64
+
65
+ route
66
+ end
67
+
68
+ sig do
69
+ params(route: Routes::Route, path: T::Array[String]).returns(
70
+ Routes::Route
71
+ )
72
+ end
73
+ def traverse_directory(route, path: [])
74
+ absolute_path = File.join(@root, path)
75
+
76
+ Dir
77
+ .entries(absolute_path)
78
+ .difference(IGNORE)
79
+ .each { |entry| visit(route, path: [*path, entry]) }
80
+
81
+ route
82
+ end
83
+
84
+ sig do
85
+ params(route: Routes::Route, path: T::Array[String]).returns(
86
+ Routes::Route
87
+ )
88
+ end
89
+ def visit_file(route, path: [])
90
+ basename = path.last.to_s
91
+ extname = File.extname(basename)
92
+
93
+ if EXTENSIONS.include?(extname)
94
+ case basename.delete_suffix(extname)
95
+ when "page"
96
+ route.page = basename
97
+ when "layout"
98
+ route.layout = basename
99
+ when "404"
100
+ route.not_found = basename
101
+ end
102
+ end
103
+
104
+ route
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,58 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Mayu
5
+ module Routing
6
+ class Matcher
7
+ extend T::Sig
8
+
9
+ class RouteError < StandardError
10
+ end
11
+
12
+ sig { params(root: Routes::Route).void }
13
+ def initialize(root)
14
+ @root = root
15
+ end
16
+
17
+ sig { params(path: String).void }
18
+ def match(path)
19
+ layouts = []
20
+ parts = []
21
+ params = {}
22
+
23
+ not_found = T.let(nil, T.nilable(String))
24
+
25
+ found =
26
+ path
27
+ .delete_prefix("/")
28
+ .split("/")
29
+ .reduce(@root) do |curr, part|
30
+ layouts.push(File.join("", *parts, curr.layout)) if curr.layout
31
+
32
+ if curr.not_found
33
+ not_found = File.join("", *parts, curr.not_found)
34
+ end
35
+
36
+ match = curr.match(part)
37
+
38
+ break unless match
39
+
40
+ if match.is_a?(Routes::Param)
41
+ params[match.name.to_sym] = part
42
+ parts.push(":#{part}")
43
+ else
44
+ parts.push(part)
45
+ end
46
+
47
+ match
48
+ end
49
+
50
+ return { layouts:, component: File.join("", *parts), params: } if found
51
+
52
+ return { layouts: [], component: not_found, params: } if not_found
53
+
54
+ raise RouteError, "No 404 page configured, put one in #{@root}"
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,85 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Mayu
5
+ module Routing
6
+ module Routes
7
+ class Route
8
+ extend T::Sig
9
+
10
+ sig { returns(T.nilable(String)) }
11
+ attr_accessor :page
12
+ sig { returns(T.nilable(String)) }
13
+ attr_accessor :layout
14
+ sig { returns(T.nilable(String)) }
15
+ attr_accessor :not_found
16
+
17
+ sig { void }
18
+ def initialize
19
+ @page = nil
20
+ @layout = nil
21
+ @not_found = nil
22
+ @named = T.let({}, T::Hash[String, Named])
23
+ @params = T.let([], T::Array[Param])
24
+ end
25
+
26
+ sig { params(route: Route).void }
27
+ def add_route(route)
28
+ case route
29
+ when Named
30
+ @named[route.name] = route
31
+ when Param
32
+ @params.push(route)
33
+ else
34
+ raise TypeError, "Unknown route type: #{route.class}"
35
+ end
36
+ end
37
+
38
+ sig { params(part: String).returns(T.nilable(Route)) }
39
+ def match(part)
40
+ @named.fetch(part) { @params.find { _1.match?(part) } }
41
+ end
42
+
43
+ sig { params(part: String).returns(T::Boolean) }
44
+ def match?(part)
45
+ true
46
+ end
47
+ end
48
+
49
+ class Named < Route
50
+ sig { returns(String) }
51
+ attr_reader :name
52
+
53
+ sig { params(name: String).void }
54
+ def initialize(name)
55
+ super()
56
+ @name = name
57
+ end
58
+
59
+ sig { params(part: String).returns(T::Boolean) }
60
+ def match?(part)
61
+ part == @name
62
+ end
63
+ end
64
+
65
+ class Param < Route
66
+ sig { returns(String) }
67
+ attr_reader :name
68
+ sig { returns(Regexp) }
69
+ attr_reader :format
70
+
71
+ sig { params(name: String, format: Regexp).void }
72
+ def initialize(name, format: /\A\d+\z/)
73
+ super()
74
+ @name = name
75
+ @format = format
76
+ end
77
+
78
+ sig { params(part: String).returns(T::Boolean) }
79
+ def match?(part)
80
+ @format.match?(part)
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,17 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "routing/routes"
5
+ require_relative "routing/builder"
6
+ require_relative "routing/matcher"
7
+
8
+ module Mayu
9
+ module Routing
10
+ end
11
+ end
12
+
13
+ # root = Routing::Builder.build(File.join(__dir__, "example", "app", "pages"))
14
+ # matcher = Routing::Matcher.new(root)
15
+ # p matcher.match("/pokemon")
16
+ # p matcher.match("/pokemon/123")
17
+ # p matcher.match("/pokemon/123/asd")