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,26 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ require "fileutils"
5
+ require_relative "base"
6
+
7
+ module Mayu
8
+ module Resources
9
+ module Generators
10
+ class CopyFile < Base
11
+ extend T::Sig
12
+
13
+ sig { params(source_path: String).void }
14
+ def initialize(source_path)
15
+ @source_path = source_path
16
+ end
17
+
18
+ sig { override.params(target_path: String).void }
19
+ def process(target_path)
20
+ return if File.exist?(target_path)
21
+ FileUtils.copy_file(@source_path, target_path)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ require "image_size"
5
+ require "base64"
6
+ require "shellwords"
7
+ require_relative "base"
8
+
9
+ module Mayu
10
+ module Resources
11
+ module Generators
12
+ class Image < Base
13
+ extend T::Sig
14
+
15
+ sig do
16
+ params(
17
+ source_path: String,
18
+ version: Types::Image::ImageDescriptor
19
+ ).void
20
+ end
21
+ def initialize(source_path, version)
22
+ @source_path = source_path
23
+ @version = version
24
+ end
25
+
26
+ sig { override.params(target_path: String).void }
27
+ def process(target_path)
28
+ return if File.exist?(target_path)
29
+
30
+ Console.logger.info(
31
+ self,
32
+ "Generating #{target_path} from #{@source_path}"
33
+ )
34
+
35
+ case @version.format
36
+ when :webp
37
+ convert_webp(
38
+ source_path: @source_path,
39
+ quality: 80, # typical quality,
40
+ width: @version.width,
41
+ target_path: target_path
42
+ )
43
+ else
44
+ convert_generic(
45
+ source_path: @source_path,
46
+ quality: 80, # typical quality,
47
+ width: @version.width,
48
+ target_path: target_path
49
+ )
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ sig do
56
+ params(
57
+ source_path: String,
58
+ quality: Integer,
59
+ width: Integer,
60
+ target_path: String
61
+ ).void
62
+ end
63
+ def convert_webp(source_path:, quality:, width:, target_path:)
64
+ system(
65
+ Shellwords.shelljoin(
66
+ [
67
+ "cwebp",
68
+ "-q",
69
+ "#{quality}",
70
+ "-resize",
71
+ "#{width}",
72
+ "0",
73
+ source_path,
74
+ "-o",
75
+ target_path
76
+ ]
77
+ ) + " 2> /dev/null"
78
+ ) or raise "Could not generate #{target_path} from #{source_path}"
79
+ end
80
+
81
+ sig do
82
+ params(
83
+ source_path: String,
84
+ quality: Integer,
85
+ width: Integer,
86
+ target_path: String
87
+ ).void
88
+ end
89
+ def convert_generic(source_path:, quality:, width:, target_path:)
90
+ system(
91
+ Shellwords.shelljoin(
92
+ [
93
+ "convert",
94
+ source_path,
95
+ "-adaptive-resize",
96
+ "#{width}",
97
+ "-strip",
98
+ target_path
99
+ ]
100
+ ) + " 2> /dev/null"
101
+ ) or raise "Could not generate #{target_path} from #{source_path}"
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ require "fileutils"
5
+ require_relative "base"
6
+
7
+ module Mayu
8
+ module Resources
9
+ module Generators
10
+ class WriteFile < Base
11
+ extend T::Sig
12
+
13
+ sig { params(contents: String, compress: T::Boolean).void }
14
+ def initialize(contents:, compress:)
15
+ @contents = contents
16
+ @compress = compress
17
+ end
18
+
19
+ sig { override.params(target_path: String).void }
20
+ def process(target_path)
21
+ write_file(target_path, @contents)
22
+
23
+ if @compress
24
+ write_file(target_path + ".br", Brotli.deflate(@contents))
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ sig { params(path: String, content: String).void }
31
+ def write_file(path, content)
32
+ return if File.exist?(path)
33
+ Console.logger.info(self, "Writing #{path}")
34
+ File.write(path, content)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ require "listen"
5
+ require "async"
6
+ require "async/queue"
7
+ require "async/task"
8
+ require "thread"
9
+
10
+ module Mayu
11
+ module Resources
12
+ module HotSwap
13
+ module FileWatcher
14
+ extend T::Sig
15
+
16
+ class Event < T::Struct
17
+ const :modified, T::Array[String]
18
+ const :added, T::Array[String]
19
+ const :removed, T::Array[String]
20
+ end
21
+
22
+ sig do
23
+ params(
24
+ root: String,
25
+ dirs: T::Array[String],
26
+ task: Async::Task,
27
+ block: T.proc.params(arg0: Event).void
28
+ ).returns(Async::Task)
29
+ end
30
+ def self.watch(
31
+ root = Dir.pwd,
32
+ dirs = [""],
33
+ task: Async::Task.current,
34
+ &block
35
+ )
36
+ root = File.expand_path(root)
37
+ queue = Thread::Queue.new
38
+ paths = dirs.map { File.join(root, _1) }
39
+
40
+ listener =
41
+ T.let(
42
+ T
43
+ .unsafe(Listen)
44
+ .to(*paths) do |modified, added, removed|
45
+ Event
46
+ .new(
47
+ modified: modified.map { _1.delete_prefix(root) },
48
+ added: added.map { _1.delete_prefix(root) },
49
+ removed: removed.map { _1.delete_prefix(root) }
50
+ )
51
+ .then { queue.enq(_1) }
52
+ end,
53
+ Listen::Listener
54
+ )
55
+
56
+ listener.start
57
+
58
+ Console.logger.info("Watching directories for changes:", *paths)
59
+
60
+ task.async do
61
+ loop { block.call(queue.deq) }
62
+ ensure
63
+ listener.stop
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ require_relative "hot_swap/file_watcher"
5
+ require_relative "registry"
6
+
7
+ module Mayu
8
+ module Resources
9
+ module HotSwap
10
+ extend T::Sig
11
+
12
+ sig do
13
+ params(registry: Registry, block: T.proc.void).returns(Async::Task)
14
+ end
15
+ def self.start(registry, &block)
16
+ FileWatcher.watch(registry.root, ["app"]) do |event|
17
+ Console.logger.info(self, "\e[33mSwapping code\e[0m")
18
+ start_at = Time.now.to_f
19
+
20
+ visited = T::Set[String].new
21
+
22
+ event.modified.each do |path|
23
+ registry.reload_resource(path, visited:)
24
+ end
25
+
26
+ event.added.each do |path|
27
+ registry.reload_resource(
28
+ path,
29
+ visited:,
30
+ add: path.start_with?("/app/pages/")
31
+ )
32
+ end
33
+
34
+ event.removed.each { |path| registry.unload_resource(path, visited:) }
35
+
36
+ Console.logger.info(
37
+ self,
38
+ format("\e[33mSwapped code in %.3fs\e[0m", Time.now.to_f - start_at)
39
+ )
40
+
41
+ yield
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,210 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ require "base64"
5
+ require "zlib"
6
+ require_relative "dependency_graph"
7
+
8
+ module Mayu
9
+ module Resources
10
+ class MermaidExporter
11
+ extend T::Sig
12
+
13
+ GRAPH_DIRECTION = "LR" # Left to right
14
+
15
+ sig { params(graph: DependencyGraph).void }
16
+ def initialize(graph)
17
+ @graph = graph
18
+ end
19
+
20
+ sig { returns(String) }
21
+ def to_url
22
+ data = {
23
+ code: to_source,
24
+ mermaid: JSON.generate({ theme: "dark" }),
25
+ updateEditor: false,
26
+ autoSync: false,
27
+ updateDiagram: false,
28
+ editorMode: "code"
29
+ }
30
+
31
+ pako =
32
+ data
33
+ .then { JSON.generate(_1) }
34
+ .then { Zlib.deflate(_1) }
35
+ .then { Base64.urlsafe_encode64(_1) }
36
+
37
+ "https://mermaid.live/view#pako:#{pako}"
38
+ end
39
+
40
+ sig { returns(String) }
41
+ def to_source
42
+ StringIO.new.tap { write_source(_1) }.tap(&:rewind).read.to_s
43
+ end
44
+
45
+ sig { params(out: StringIO).returns(StringIO) }
46
+ def write_source(out)
47
+ entries =
48
+ @graph
49
+ .overall_order(only_leaves: false)
50
+ .filter { @graph.get_resource(_1)&.exists? }
51
+
52
+ tree = make_tree(entries.map { _1.split("/") })
53
+ out.puts "graph #{GRAPH_DIRECTION}"
54
+
55
+ out.puts " subgraph routes"
56
+ print_routes(out, tree.dig("", "app", "pages") || {})
57
+ out.puts " end"
58
+
59
+ print_route_edges(out, tree.dig("", "app", "pages") || {})
60
+
61
+ print_subgraphs(out, tree)
62
+
63
+ entries.each do |entry|
64
+ @graph
65
+ .direct_dependencies_of(entry)
66
+ .filter { @graph.get_resource(_1)&.exists? }
67
+ .each { |dep| out.puts " #{encode(entry)}-->#{encode(dep)}" }
68
+ rescue StandardError
69
+ next
70
+ end
71
+
72
+ entries.each do |entry|
73
+ unless @graph.get_resource(entry)&.exists?
74
+ out.puts " class #{encode(entry)} NonExistant"
75
+ end
76
+
77
+ filetype_class(entry)&.tap do
78
+ out.puts " class #{encode(entry)} #{_1}"
79
+ end
80
+ end
81
+
82
+ out.puts <<~EOF.gsub(/^/, " ")
83
+ style routes stroke:#09c,stroke-width:5,fill:#f0f;
84
+ classDef cluster fill:#0003;
85
+ classDef Ruby fill:#600,stroke:#900,stroke-width:3px;
86
+ classDef Image fill:#069,stroke:#09c,stroke-width:3px;
87
+ classDef CSS fill:#063,stroke:#096,stroke-width:3px;
88
+ classDef NonExistant opacity:50%,stroke-dasharray:5px;
89
+ linkStyle default fill:transparent,opacity:50%;
90
+ EOF
91
+
92
+ out
93
+ end
94
+
95
+ private
96
+
97
+ sig { params(path: String).returns(T.nilable(String)) }
98
+ def filetype_class(path)
99
+ case File.extname(path)
100
+ when ".rb"
101
+ "Ruby"
102
+ when ".css"
103
+ "CSS"
104
+ when ".png"
105
+ "Image"
106
+ end
107
+ end
108
+
109
+ sig { params(str: String).returns(String) }
110
+ def encode(str)
111
+ str.gsub("/", "__").gsub("[", "__").gsub("]", "__")
112
+ end
113
+
114
+ sig { params(str: String).returns(String) }
115
+ def escape(str)
116
+ str.gsub(/\W/) { |ch| ch.codepoints.map { |cp| "##{cp};" }.join }
117
+ end
118
+
119
+ sig { params(str: String).returns(String) }
120
+ def display_name(str)
121
+ case File.extname(str)
122
+ when ".rb"
123
+ "fa:fa-gem #{str}&nbsp;"
124
+ when ".css"
125
+ "fab:fa-css3 #{str}&nbsp;"
126
+ when ".png"
127
+ "fa:fa-image #{str}&nbsp;"
128
+ else
129
+ str
130
+ end
131
+ end
132
+
133
+ sig do
134
+ params(out: StringIO, node: T.untyped, path: T::Array[String]).void
135
+ end
136
+ def print_routes(out, node, path = [])
137
+ node.each do |key, value|
138
+ path2 = path + [key]
139
+
140
+ if value.is_a?(String)
141
+ if key == "page.rb" || key == "page.haml"
142
+ pathstr = path.flatten.join("/").sub(%r{\A/?}, "/")
143
+ out.puts " ROUTE__#{encode(value)}[#{pathstr.inspect}]"
144
+ end
145
+ else
146
+ print_routes(out, value, path2)
147
+ end
148
+ end
149
+ end
150
+
151
+ sig do
152
+ params(out: StringIO, node: T.untyped, path: T::Array[String]).void
153
+ end
154
+ def print_route_edges(out, node, path = [])
155
+ node.each do |key, value|
156
+ path2 = path + [key]
157
+
158
+ if value.is_a?(String)
159
+ if key == "page.rb" || key == "page.haml"
160
+ pathstr = path.flatten.join("/").sub(%r{\A/?}, "/")
161
+ out.puts " ROUTE__#{encode(value)}-->#{encode(value)}"
162
+ end
163
+ else
164
+ print_route_edges(out, value, path2)
165
+ end
166
+ end
167
+ end
168
+
169
+ sig do
170
+ params(out: StringIO, node: T.untyped, path: T::Array[String]).void
171
+ end
172
+ def print_subgraphs(out, node, path = [])
173
+ level = path.length
174
+ indent = " " * level.succ
175
+
176
+ node.each do |key, value|
177
+ path2 = path + [key]
178
+
179
+ if value.is_a?(String)
180
+ out.puts "#{indent}#{encode(value)}[#{display_name(key).inspect}]"
181
+ else
182
+ pathstr = path2.flatten.join("/").sub(%r{\A/?}, "/")
183
+ out.puts "#{indent}subgraph PATH#{encode(pathstr)}[#{pathstr.inspect}]"
184
+ print_subgraphs(out, value, path2)
185
+ out.puts "#{indent}end"
186
+ end
187
+ end
188
+ end
189
+
190
+ sig do
191
+ params(entries: T::Array[T::Array[String]], level: Integer).returns(
192
+ T.untyped
193
+ )
194
+ end
195
+ def make_tree(entries, level = 0)
196
+ entries
197
+ .group_by { _1[level] }
198
+ .transform_values do |paths|
199
+ paths
200
+ .partition { _1.length.pred <= level.succ }
201
+ .then do |leaves, branches|
202
+ leaves.each_with_object(
203
+ make_tree(branches, level + 1)
204
+ ) { |leaf, obj| obj[leaf.last] = leaf.join("/") }
205
+ end
206
+ end
207
+ end
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ require "sorbet-runtime"
5
+ require "msgpack"
6
+ require_relative "resolver"
7
+ require_relative "dependency_graph"
8
+ require_relative "resource"
9
+ require_relative "types"
10
+ require_relative "assets"
11
+
12
+ MessagePack::DefaultFactory.register_type(0x00, Symbol)
13
+
14
+ module Mayu
15
+ module Resources
16
+ class Registry
17
+ EXTENSIONS = T.let(["", ".rb", ".haml"].freeze, T::Array[String])
18
+
19
+ extend T::Sig
20
+
21
+ sig { returns(String) }
22
+ attr_reader :root
23
+ sig { returns(DependencyGraph) }
24
+ attr_reader :dependency_graph
25
+
26
+ sig { params(root: String).void }
27
+ def initialize(root:)
28
+ @root = T.let(File.expand_path(root), String)
29
+ @dependency_graph = T.let(DependencyGraph.new, DependencyGraph)
30
+ @resolver =
31
+ T.let(
32
+ Resolver::Filesystem.new(@root, extensions: EXTENSIONS),
33
+ Resolver::Base
34
+ )
35
+ @assets = T.let(Assets.new, T.nilable(Assets))
36
+ end
37
+
38
+ sig { params(dumped: String, root: String).returns(Registry) }
39
+ def self.load(dumped, root:)
40
+ MessagePack.unpack(dumped) => { type: "registry", data: String => data }
41
+ registry =
42
+ T.cast(
43
+ Marshal.load(
44
+ data,
45
+ ->(obj) do
46
+ obj.instance_variable_set(:@root, root) if obj.is_a?(Registry)
47
+ obj
48
+ end
49
+ ),
50
+ Registry
51
+ )
52
+
53
+ registry
54
+ end
55
+
56
+ sig { returns(String) }
57
+ def dump
58
+ MessagePack.pack(type: "registry", data: Marshal.dump(self))
59
+ end
60
+
61
+ MarshalFormat = T.type_alias { [String, T::Hash[String, String]] }
62
+
63
+ sig { returns(MarshalFormat) }
64
+ def marshal_dump
65
+ [Marshal.dump(@dependency_graph), @resolver.resolved_paths]
66
+ end
67
+
68
+ sig { params(args: MarshalFormat).void }
69
+ def marshal_load(args)
70
+ dependency_graph =
71
+ T.cast(
72
+ Marshal.load(
73
+ args[0],
74
+ ->(obj) do
75
+ if obj.is_a?(Resource)
76
+ obj.instance_variable_set(:@registry, self)
77
+ end
78
+ obj
79
+ end
80
+ ),
81
+ DependencyGraph
82
+ )
83
+
84
+ @dependency_graph = dependency_graph
85
+ @resolver = Resolver::Static.new(args[1])
86
+ end
87
+
88
+ sig { params(path: String).returns(String) }
89
+ def absolute_path(path)
90
+ File.join(@root, File.expand_path(path, "/"))
91
+ end
92
+
93
+ sig { returns(String) }
94
+ def mermaid_url
95
+ @dependency_graph.to_mermaid_url
96
+ end
97
+
98
+ sig { params(filename: String, timeout: Integer, task: Async::Task).void }
99
+ def wait_for_asset(filename, timeout: 2, task: Async::Task.current)
100
+ return unless @assets
101
+
102
+ task.with_timeout(timeout) { @assets.wait_for(filename) }
103
+ end
104
+
105
+ sig do
106
+ params(
107
+ asset_dir: String,
108
+ concurrency: Integer,
109
+ forever: T::Boolean
110
+ ).returns(Async::Task)
111
+ end
112
+ def generate_assets(asset_dir, concurrency:, forever:)
113
+ if @assets
114
+ @assets.run(asset_dir, concurrency:, forever:)
115
+ else
116
+ raise "Assets can't be generated in production"
117
+ end
118
+ end
119
+
120
+ sig do
121
+ params(path: String, visited: T::Set[String], add: T::Boolean).void
122
+ end
123
+ def reload_resource(path, visited: T::Set[String].new, add: false)
124
+ unless @dependency_graph.has_node?(path)
125
+ add_resource(path) if add
126
+ return
127
+ end
128
+
129
+ reload_resources(
130
+ [path, *@dependency_graph.dependants_of(path)],
131
+ visited:
132
+ )
133
+ end
134
+
135
+ sig { params(path: String, visited: T::Set[String]).void }
136
+ def unload_resource(path, visited: T::Set[String].new)
137
+ return unless @dependency_graph.has_node?(path)
138
+
139
+ dependants = @dependency_graph.dependants_of(path)
140
+
141
+ Console.logger.info(self, "Unloading resource, #{path}")
142
+
143
+ @dependency_graph.delete_node(path)
144
+
145
+ reload_resources(dependants, visited:)
146
+ end
147
+
148
+ sig { params(path: String, source: String).returns(Resource) }
149
+ def load_resource(path, source = "/")
150
+ resolved_path = @resolver.resolve(path, source)
151
+ add_resource(resolved_path)
152
+ end
153
+
154
+ sig { params(path: String).returns(Resource) }
155
+ def add_resource(path)
156
+ if resource = @dependency_graph.get_resource(path)
157
+ return resource
158
+ end
159
+
160
+ resource = Resource.new(registry: self, path:)
161
+
162
+ @dependency_graph.add_node(resource.path, resource)
163
+
164
+ resource.assets.each { |asset| @assets.add(asset) } if @assets
165
+
166
+ Console.logger.info(
167
+ self,
168
+ "Loaded #{resource.type.name} from #{resource.path}"
169
+ )
170
+ resource
171
+ end
172
+
173
+ private
174
+
175
+ sig { params(paths: T::Enumerable[String], visited: T::Set[String]).void }
176
+ def reload_resources(paths, visited:)
177
+ paths
178
+ .select { visited.add?(_1) }
179
+ .map { @dependency_graph.get_resource(_1) }
180
+ .compact
181
+ .each do |resource|
182
+ Console.logger.info(self, "Reloading resource: #{resource.path}")
183
+ @dependency_graph.delete_connections(resource.path)
184
+ resource.load_type
185
+ resource.assets.each { |asset| @assets.add(asset) } if @assets
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end