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,57 @@
1
+ # typed: strict
2
+
3
+ module Mayu
4
+ class RefCounter
5
+ extend T::Sig
6
+ extend T::Generic
7
+
8
+ Elem = type_member
9
+
10
+ sig { void }
11
+ def initialize
12
+ @refs = T.let(Hash.new { |h, k| h[k] = 0 }, T::Hash[Elem, Integer])
13
+ end
14
+
15
+ sig { params(key: Elem).returns(Integer) }
16
+ def count(key)
17
+ @refs.fetch(key, 0)
18
+ end
19
+
20
+ sig { returns(T::Array[Elem]) }
21
+ def keys
22
+ @refs.sort_by { _2 }.map(&:first)
23
+ end
24
+
25
+ sig { params(key: Elem).void }
26
+ def acquire!(key)
27
+ @refs[key] = @refs[key].to_i + 1
28
+ end
29
+
30
+ sig do
31
+ type_parameters(:R)
32
+ .params(key: Elem, block: T.proc.returns(T.type_parameter(:R)))
33
+ .returns(T.type_parameter(:R))
34
+ end
35
+ def acquire(key, &block)
36
+ acquire!(key)
37
+
38
+ begin
39
+ yield
40
+ ensure
41
+ release(key)
42
+ end
43
+ end
44
+
45
+ sig { params(key: Elem).void }
46
+ def release(key)
47
+ count = @refs.fetch(key, nil)
48
+ return unless count
49
+
50
+ if count > 1
51
+ @refs[key] = count - 1
52
+ else
53
+ @refs.delete(key)
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,14 @@
1
+ # Mayu::Resources
2
+
3
+ This module contains classes for loading resources.
4
+
5
+ A resource is any type of file that can be used in an app,
6
+ usually a component, stylesheet or image.
7
+
8
+ In development mode, it is possible to watch some directories
9
+ for changes and reload resources dynamically as they are updated.
10
+
11
+ For production, all the resources will be serialized using
12
+ [Marshal](https://docs.ruby-lang.org/en/master/Marshal.html),
13
+ and static fileswill be generated during build time, and then
14
+ loaded in runtime.
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ require_relative "generators/image"
5
+ require_relative "generators/copy_file"
6
+ require_relative "generators/write_file"
7
+
8
+ module Mayu
9
+ module Resources
10
+ class Asset
11
+ extend T::Sig
12
+
13
+ class Status < T::Enum
14
+ enums do
15
+ Pending = new
16
+ Processing = new
17
+ Done = new
18
+ Failed = new
19
+ end
20
+ end
21
+
22
+ sig { returns(String) }
23
+ attr_reader :filename
24
+ sig { returns(Status) }
25
+ attr_reader :status
26
+ sig { returns(Generators::Base) }
27
+ attr_reader :generator
28
+
29
+ sig { params(filename: String, generator: Generators::Base).void }
30
+ def initialize(filename, generator)
31
+ @filename = filename
32
+ @generator = generator
33
+ @status = T.let(Status::Pending, Status)
34
+ end
35
+
36
+ sig { returns(T::Boolean) }
37
+ def pending? = @status == Status::Pending
38
+ sig { returns(T::Boolean) }
39
+ def processing? = @status == Status::Processing
40
+ sig { returns(T::Boolean) }
41
+ def done? = @status == Status::Done
42
+ sig { returns(T::Boolean) }
43
+ def failed? = @status == Status::Failed
44
+
45
+ sig { params(asset_dir: String).returns(T::Boolean) }
46
+ def process(asset_dir)
47
+ return false unless pending?
48
+ @status = Status::Processing
49
+ @generator.process(File.join(asset_dir, filename))
50
+ rescue StandardError
51
+ @status = Status::Failed
52
+ raise
53
+ else
54
+ @status = Status::Done
55
+ true
56
+ end
57
+
58
+ MarshalFormat = T.type_alias { [String] }
59
+
60
+ sig { returns(MarshalFormat) }
61
+ def marshal_dump
62
+ [@filename]
63
+ end
64
+
65
+ sig { params(args: MarshalFormat).void }
66
+ def marshal_load(args)
67
+ @filename = args.first
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ require "async/variable"
5
+ require "async/queue"
6
+ require "async/semaphore"
7
+
8
+ module Mayu
9
+ module Resources
10
+ class Assets
11
+ extend T::Sig
12
+
13
+ sig { void }
14
+ def initialize
15
+ @queue = T.let(Async::Queue.new, Async::Queue)
16
+ @results = T.let({}, T::Hash[String, Async::Variable])
17
+ @assets = T.let({}, T::Hash[String, Asset])
18
+ end
19
+
20
+ sig { params(filename: String).void }
21
+ def wait_for(filename)
22
+ (@results[filename] ||= Async::Variable.new).wait
23
+ end
24
+
25
+ sig { params(asset: Asset).void }
26
+ def add(asset)
27
+ @assets[asset.filename] ||= asset
28
+ @queue.enqueue(asset)
29
+ end
30
+
31
+ sig do
32
+ params(
33
+ asset_dir: String,
34
+ concurrency: Integer,
35
+ forever: T::Boolean,
36
+ task: Async::Task
37
+ ).returns(Async::Task)
38
+ end
39
+ def run(
40
+ asset_dir,
41
+ concurrency:,
42
+ forever: false,
43
+ task: Async::Task.current
44
+ )
45
+ task.async do
46
+ semaphore = Async::Semaphore.new(concurrency)
47
+
48
+ while forever || !@queue.empty?
49
+ process(@queue.dequeue, asset_dir, semaphore)
50
+ end
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ sig do
57
+ params(
58
+ asset: Asset,
59
+ asset_dir: String,
60
+ semaphore: Async::Semaphore
61
+ ).void
62
+ end
63
+ def process(asset, asset_dir, semaphore)
64
+ semaphore.async do
65
+ if asset.process(asset_dir)
66
+ var = (@results[asset.filename] ||= Async::Variable.new)
67
+ var.resolve unless var.resolved?
68
+ end
69
+ rescue => e
70
+ Console.logger.error(self, e)
71
+ raise
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,306 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ require "tsort"
5
+ require "set"
6
+ require "cgi"
7
+ require_relative "mermaid_exporter"
8
+ require_relative "dot_exporter"
9
+
10
+ module Mayu
11
+ module Resources
12
+ class DependencyGraph
13
+ extend T::Sig
14
+ # This is basically a reimplementation of this library:
15
+ # https://github.com/jriecken/dependency-graph
16
+
17
+ class Direction < T::Enum
18
+ enums do
19
+ Incoming = new(:incoming)
20
+ Outgoing = new(:outgoing)
21
+ end
22
+ end
23
+
24
+ class Node
25
+ extend T::Sig
26
+
27
+ sig { returns(Resource) }
28
+ attr_reader :resource
29
+ sig { returns(T::Set[String]) }
30
+ attr_reader :incoming
31
+ sig { returns(T::Set[String]) }
32
+ attr_reader :outgoing
33
+
34
+ sig { params(resource: Resource).void }
35
+ def initialize(resource)
36
+ @resource = resource
37
+ @incoming = T.let(Set.new, T::Set[String])
38
+ @outgoing = T.let(Set.new, T::Set[String])
39
+ end
40
+
41
+ sig { params(id: String).void }
42
+ def delete(id)
43
+ @incoming.delete(id)
44
+ @outgoing.delete(id)
45
+ end
46
+
47
+ MarshalFormat =
48
+ T.type_alias { [Resource, T::Set[String], T::Set[String]] }
49
+
50
+ sig { returns(MarshalFormat) }
51
+ def marshal_dump
52
+ [@resource, @incoming, @outgoing]
53
+ end
54
+
55
+ sig { params(dumped: MarshalFormat).void }
56
+ def marshal_load(dumped)
57
+ @resource, @incoming, @outgoing = dumped
58
+ end
59
+ end
60
+
61
+ sig { void }
62
+ def initialize
63
+ @nodes = T.let({}, T::Hash[String, Node])
64
+ end
65
+
66
+ sig { returns(Integer) }
67
+ def size = @nodes.size
68
+
69
+ sig { params(id: String).returns(T::Boolean) }
70
+ def include?(id) = @nodes.include?(id)
71
+
72
+ sig { params(id: String, resource: Resource).returns(Resource) }
73
+ def add_node(id, resource)
74
+ (@nodes[id] ||= Node.new(resource)).resource
75
+ end
76
+
77
+ sig { params(id: String).void }
78
+ def delete_node(id)
79
+ return unless @nodes.include?(id)
80
+ @nodes.delete(id)
81
+ delete_connections(id)
82
+ end
83
+
84
+ sig { params(id: String).void }
85
+ def delete_connections(id)
86
+ @nodes.each { |node| node.delete(id) }
87
+ end
88
+
89
+ sig { params(id: String).returns(T.nilable(Resource)) }
90
+ def get_resource(id)
91
+ @nodes[id]&.resource
92
+ end
93
+
94
+ sig { params(id: String).returns(T::Boolean) }
95
+ def has_node?(id)
96
+ @nodes.include?(id)
97
+ end
98
+
99
+ sig { params(source_id: String, target_id: String).void }
100
+ def add_dependency(source_id, target_id)
101
+ with_source_and_target(source_id, target_id) do |source, target|
102
+ source.outgoing.add(target_id)
103
+ target.incoming.add(source_id)
104
+ end
105
+ end
106
+
107
+ sig { params(source_id: String, target_id: String).void }
108
+ def remove_dependency(source_id, target_id)
109
+ with_source_and_target(source_id, target_id) do |source, target|
110
+ source.outgoing.delete(target_id)
111
+ source.incoming.delete(source_id)
112
+ end
113
+ end
114
+
115
+ sig { params(id: String).returns(T::Array[String]) }
116
+ def direct_dependencies_of(id)
117
+ @nodes.fetch(id).outgoing.to_a
118
+ end
119
+
120
+ sig { params(id: String).returns(T::Array[String]) }
121
+ def direct_dependants_of(id)
122
+ @nodes.fetch(id).incoming.to_a
123
+ end
124
+
125
+ sig do
126
+ params(
127
+ id: String,
128
+ started_at: T.nilable(String),
129
+ only_leaves: T::Boolean,
130
+ block: T.nilable(T.proc.params(arg0: String).returns(T::Boolean))
131
+ ).returns(T::Set[String])
132
+ end
133
+ def dependencies_of(id, started_at = nil, only_leaves: false, &block)
134
+ raise "Circular" if id == started_at
135
+
136
+ @nodes
137
+ .fetch(id)
138
+ .outgoing
139
+ .map do |dependency|
140
+ next nil unless yield dependency if block_given?
141
+
142
+ dependencies = dependencies_of(dependency, started_at || id)
143
+
144
+ if !only_leaves || dependencies.empty?
145
+ dependencies.add(dependency)
146
+ else
147
+ dependencies
148
+ end
149
+ end
150
+ .compact
151
+ .reduce(Set.new, &:merge)
152
+ end
153
+
154
+ sig do
155
+ params(
156
+ id: String,
157
+ started_at: T.nilable(String),
158
+ only_leaves: T::Boolean
159
+ ).returns(T::Set[String])
160
+ end
161
+ def dependants_of(id, started_at = nil, only_leaves: false)
162
+ raise "Circular" if id == started_at
163
+
164
+ @nodes
165
+ .fetch(id)
166
+ .incoming
167
+ .map do |dependant|
168
+ dependants = dependants_of(dependant, started_at || id)
169
+ if !only_leaves || dependants.empty?
170
+ dependants.add(dependant)
171
+ else
172
+ dependants
173
+ end
174
+ end
175
+ .reduce(Set.new, &:merge)
176
+ end
177
+
178
+ sig { returns(T::Array[String]) }
179
+ def entry_nodes
180
+ @nodes.filter { _2.incoming.empty? }.keys
181
+ end
182
+
183
+ sig { params(only_leaves: T::Boolean).returns(T::Array[String]) }
184
+ def overall_order(only_leaves: true)
185
+ TSort.tsort(
186
+ ->(&b) { @nodes.keys.each(&b) },
187
+ ->(key, &b) { @nodes[key]&.outgoing&.each(&b) }
188
+ )
189
+ end
190
+
191
+ sig { returns(String) }
192
+ def to_dot
193
+ DotExporter.new(self).to_source
194
+ end
195
+
196
+ sig { returns(String) }
197
+ def to_mermaid_source
198
+ MermaidExporter.new(self).to_source
199
+ end
200
+
201
+ sig { returns(String) }
202
+ def to_mermaid_url
203
+ MermaidExporter.new(self).to_url
204
+ end
205
+
206
+ sig { returns(T::Array[String]) }
207
+ def paths
208
+ @nodes.keys
209
+ end
210
+
211
+ sig { params(block: T.proc.params(arg0: Resource).void).void }
212
+ def each_resource(&block)
213
+ @nodes.each_value { |node| yield node.resource }
214
+ end
215
+
216
+ MarshalFormat = T.type_alias { T::Hash[String, Node] }
217
+
218
+ sig { returns(MarshalFormat) }
219
+ def marshal_dump
220
+ @nodes
221
+ end
222
+
223
+ sig { params(nodes: MarshalFormat).void }
224
+ def marshal_load(nodes)
225
+ @nodes = nodes
226
+ end
227
+
228
+ sig do
229
+ params(
230
+ id: String,
231
+ direction: Direction,
232
+ visited: T::Set[String],
233
+ block: T.proc.params(arg0: String).void
234
+ ).void
235
+ end
236
+ def dfs2(id, direction, visited: T::Set[String].new, &block)
237
+ if visited.include?(id)
238
+ return
239
+ else
240
+ visited.add(id)
241
+ end
242
+
243
+ @nodes
244
+ .fetch(id)
245
+ .send(direction.serialize)
246
+ .each { dfs2(_1, direction, visited:, &block) }
247
+
248
+ yield id
249
+ end
250
+
251
+ private
252
+
253
+ sig do
254
+ params(
255
+ source_id: String,
256
+ target_id: String,
257
+ block: T.proc.params(arg0: Node, arg1: Node).void
258
+ ).void
259
+ end
260
+ def with_source_and_target(source_id, target_id, &block)
261
+ yield(fetch_node(:source, source_id), fetch_node(:target, target_id))
262
+ end
263
+
264
+ sig { params(type: Symbol, id: String).returns(Node) }
265
+ def fetch_node(type, id)
266
+ @nodes.fetch(id) do
267
+ raise ArgumentError,
268
+ "Could not find #{type} #{id.inspect} in #{@nodes.keys.inspect}"
269
+ end
270
+ end
271
+
272
+ sig do
273
+ params(
274
+ node: Node,
275
+ direction: Direction,
276
+ block: T.proc.params(arg0: Node).void
277
+ ).void
278
+ end
279
+ def dfs(node, direction, &block)
280
+ node
281
+ .send(direction.serialize)
282
+ .each { |id| dfs(@nodes.fetch(id), direction, &block) }
283
+
284
+ yield node
285
+ end
286
+ end
287
+ end
288
+ end
289
+
290
+ # if __FILE__ == $0
291
+ # graph = Resources::DependencyGraph.new
292
+ #
293
+ # graph.add_node("a")
294
+ # graph.add_node("b")
295
+ # graph.add_node("c")
296
+ #
297
+ # p graph.size
298
+ #
299
+ # graph.add_dependency("a", "b")
300
+ # graph.add_dependency("b", "c")
301
+ # p graph.dependencies_of("a")
302
+ # p graph.dependencies_of("b")
303
+ # p graph.dependants_of("c")
304
+ # p graph.overall_order
305
+ # p graph.overall_order(only_leaves: true)
306
+ # end
@@ -0,0 +1,167 @@
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 DotExporter
11
+ extend T::Sig
12
+
13
+ sig { params(graph: DependencyGraph).void }
14
+ def initialize(graph)
15
+ @graph = graph
16
+ end
17
+
18
+ sig { returns(String) }
19
+ def to_source
20
+ StringIO.new.tap { write_source(_1) }.tap(&:rewind).read.to_s
21
+ end
22
+
23
+ sig { params(out: StringIO).returns(StringIO) }
24
+ def write_source(out)
25
+ entries = @graph.overall_order(only_leaves: false)
26
+ tree = make_tree(entries.map { _1.split("/") })
27
+
28
+ out.puts <<~EOF
29
+ strict digraph "dependency-cruiser output" {
30
+ ordering="out" rankdir="LR" splines="ortho" overlap="false" nodesep="0.16" ranksep="0.5" fontname="Helvetica-bold" fontsize="9" style="rounded,bold,filled" fillcolor="#ffffff" compound="true"
31
+ node [shape="box" style="rounded, filled" height="0.2" color="black" fillcolor="#ffffcc" fontcolor="black" fontname="Helvetica" fontsize="9"]
32
+ edge [arrowhead="normal" arrowsize="0.6" penwidth="2.0" color="#00000033" fontname="Helvetica" fontsize="9"]
33
+
34
+ EOF
35
+
36
+ entries.each do |entry|
37
+ fillcolor = "#bbfeff"
38
+ out.puts " #{entry.inspect} [label=<#{File.dirname(entry)}<BR/><B>#{File.basename(entry)}</B>> tooltip=#{File.basename(entry).inspect} fillcolor=#{fillcolor.inspect}]"
39
+
40
+ @graph
41
+ .direct_dependencies_of(entry)
42
+ .each do |dep|
43
+ color = "#e5009b99"
44
+ out.puts " #{entry.inspect} -> #{dep.inspect} [color=#{color.inspect}]"
45
+ end
46
+ end
47
+
48
+ out.puts "}"
49
+ out
50
+ end
51
+
52
+ private
53
+
54
+ sig { params(path: String).returns(T.nilable(String)) }
55
+ def filetype_class(path)
56
+ case File.extname(path)
57
+ when ".rb"
58
+ "Ruby"
59
+ when ".css"
60
+ "CSS"
61
+ when ".png"
62
+ "Image"
63
+ end
64
+ end
65
+
66
+ sig { params(str: String).returns(String) }
67
+ def encode(str)
68
+ str.gsub("/", "__").gsub("[", "__").gsub("]", "__")
69
+ end
70
+
71
+ sig { params(str: String).returns(String) }
72
+ def escape(str)
73
+ str.gsub(/\W/) { |ch| ch.codepoints.map { |cp| "##{cp};" }.join }
74
+ end
75
+
76
+ sig { params(str: String).returns(String) }
77
+ def display_name(str)
78
+ case File.extname(str)
79
+ when ".rb"
80
+ "fa:fa-gem #{str}&nbsp;"
81
+ when ".css"
82
+ "fab:fa-css3 #{str}&nbsp;"
83
+ when ".png"
84
+ "fa:fa-image #{str}&nbsp;"
85
+ else
86
+ str
87
+ end
88
+ end
89
+
90
+ sig do
91
+ params(out: StringIO, node: T.untyped, path: T::Array[String]).void
92
+ end
93
+ def print_routes(out, node, path = [])
94
+ node.each do |key, value|
95
+ path2 = path + [key]
96
+
97
+ if value.is_a?(String)
98
+ if key == "page.rb"
99
+ pathstr = path.flatten.join("/").sub(%r{\A/?}, "/")
100
+ out.puts " ROUTE__#{encode(value)}[#{pathstr.inspect}]"
101
+ end
102
+ else
103
+ print_routes(out, value, path2)
104
+ end
105
+ end
106
+ end
107
+
108
+ sig do
109
+ params(out: StringIO, node: T.untyped, path: T::Array[String]).void
110
+ end
111
+ def print_route_edges(out, node, path = [])
112
+ node.each do |key, value|
113
+ path2 = path + [key]
114
+
115
+ if value.is_a?(String)
116
+ if key == "page.rb"
117
+ pathstr = path.flatten.join("/").sub(%r{\A/?}, "/")
118
+ out.puts " ROUTE__#{encode(value)}-->#{encode(value)}"
119
+ end
120
+ else
121
+ print_route_edges(out, value, path2)
122
+ end
123
+ end
124
+ end
125
+
126
+ sig do
127
+ params(out: StringIO, node: T.untyped, path: T::Array[String]).void
128
+ end
129
+ def print_subgraphs(out, node, path = [])
130
+ level = path.length
131
+ indent = " " * level.succ
132
+
133
+ node.each do |key, value|
134
+ path2 = path + [key]
135
+
136
+ if value.is_a?(String)
137
+ out.puts "#{indent}#{encode(value)}[#{display_name(key).inspect}]"
138
+ else
139
+ pathstr = path2.flatten.join("/").sub(%r{\A/?}, "/")
140
+ out.puts "#{indent}subgraph PATH#{encode(pathstr)}[#{pathstr.inspect}]"
141
+ print_subgraphs(out, value, path2)
142
+ out.puts "#{indent}end"
143
+ end
144
+ end
145
+ end
146
+
147
+ sig do
148
+ params(entries: T::Array[T::Array[String]], level: Integer).returns(
149
+ T.untyped
150
+ )
151
+ end
152
+ def make_tree(entries, level = 0)
153
+ entries
154
+ .group_by { _1[level] }
155
+ .transform_values do |paths|
156
+ paths
157
+ .partition { _1.length.pred <= level.succ }
158
+ .then do |leaves, branches|
159
+ leaves.each_with_object(
160
+ make_tree(branches, level + 1)
161
+ ) { |leaf, obj| obj[leaf.last] = leaf.join("/") }
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ module Mayu
5
+ module Resources
6
+ module Generators
7
+ class Base
8
+ extend T::Sig
9
+ extend T::Helpers
10
+ abstract!
11
+
12
+ sig { abstract.params(target_path: String).void }
13
+ def process(target_path)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end