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
data/README.md ADDED
@@ -0,0 +1,598 @@
1
+ # <img alt="Mayu Live" width="337" src="https://user-images.githubusercontent.com/41148/192179194-f44aed92-74a3-4b59-a25e-bf2a8d313796.png">
2
+
3
+ [![Ruby Workflow Status](https://img.shields.io/github/actions/workflow/status/mayu-live/framework/.github/workflows/ruby.yml?branch=main&label=ruby&style=flat-square)](https://github.com/mayu-live/framework/actions/workflows/ruby.yml)
4
+ [![Node Workflow Status](https://img.shields.io/github/actions/workflow/status/mayu-live/framework/.github/workflows/node.js.yml?branch=main&label=js&style=flat-square)](https://github.com/mayu-live/framework/actions/workflows/node.js.yml)
5
+ [![GitHub commit activity](https://img.shields.io/github/commit-activity/w/mayu-live/framework/main?style=flat-square)](https://github.com/mayu-live/framework/commits)
6
+ [![License AGPL-3.0](https://img.shields.io/github/license/mayu-live/framework?style=flat-square)](https://github.com/mayu-live/framework/blob/main/COPYING) ![Status: Experimental](https://img.shields.io/badge/status-experimental-critical?style=flat-square)
7
+
8
+ [Documentation](https://mayu.live/docs)
9
+
10
+ # Description
11
+
12
+ Mayu is a live streaming server side component-based
13
+ VirtualDOM rendering framework written in Ruby.
14
+
15
+ Everything runs on the server, except for a tiny little runtime that
16
+ deals with the connection to the server and updates the DOM.
17
+
18
+ It is very early in development and nothing is guaranteed to work.
19
+ Still trying to figure out how to make a framework that is both
20
+ easy to use and fun to work with.
21
+
22
+ Some parts are quite messy and some files are very long.
23
+ This is fine. I like to paint with a broad brush until
24
+ things are put in place and things feel right.
25
+
26
+ A starter kit is available at
27
+ [github.com/mayu-live/starter](https://github.com/mayu-live/starter)!
28
+ If you have all dependencies installed, you will be able to deploy
29
+ an app to [Fly.io](https://fly.io/) within a few minutes without
30
+ having to configure anything!
31
+
32
+ ### Core features:
33
+
34
+ - 100% Ruby
35
+ - 100% Server Side
36
+ - 100% Async
37
+ - Interactive web apps without JavaScript
38
+ - Hot-reloading in dev
39
+ - Automatic asset handling
40
+ - Built-in metrics
41
+ - File-system routing inspired by [Next.js](https://nextjs.org/docs/routing/introduction)
42
+ - Designed for edge deployments
43
+ - Powerful and compact templating using [Haml](https://haml.info/)
44
+ - One component per file
45
+ - One file per component
46
+ - Lazy loading, prefetch hints, HTTP/2, caching
47
+
48
+ ## Table of contents
49
+
50
+ - [Description](#description)
51
+ - [Getting started](#getting-started)
52
+ - [Install dependencies](#install-dependencies)
53
+ - [Start the example app](#start-the-example-app)
54
+ - [Run the tests](#run-the-tests)
55
+ - [Features](#features)
56
+ - [100% server side](#100-server-side)
57
+ - [100% async](#100-async)
58
+ - [Components](#components)
59
+ - [CSS modules](#css-modules)
60
+ - [State management](#state-management)
61
+ - [Path based routing](#path-based-routing)
62
+ - [Hot reloading](#hot-reloading)
63
+ - [Optimized data transfer](#optimized-data-transfer)
64
+ - [Realtime metrics](#realtime-metrics)
65
+ - [Haml](#haml)
66
+ - [Implementation notes](#implementation-notes)
67
+ - [Tests](#tests)
68
+ - [Virtual DOM](#virtual-dom)
69
+ - [Server](#server)
70
+ - [Development server](#development-server)
71
+ - [Production server](#production-server)
72
+ - [Static typing](#static-typing)
73
+ - [Contributing](#contributing)
74
+
75
+ # Getting started
76
+
77
+ ## Install dependencies
78
+
79
+ Make sure that you have installed [Ruby](https://www.ruby-lang.org/en/downloads/)
80
+ and [NodeJS](https://nodejs.org/en/download/).
81
+ The required versions are specified in the file `.tool-versions`
82
+ in the project root.
83
+
84
+ [ImageMagick](https://github.com/ImageMagick/ImageMagick) and
85
+ [libwebp](https://chromium.googlesource.com/webm/libwebp) are
86
+ also required for resizing images.
87
+
88
+ Install Ruby dependencies:
89
+
90
+ bundle install
91
+
92
+ Install node dependencies:
93
+
94
+ npm install
95
+
96
+ Build browser runtime:
97
+
98
+ npm run build
99
+
100
+ Start the example app
101
+
102
+ cd example
103
+ bundle install
104
+ bin/mayu dev
105
+
106
+ Now, open https://localhost:9292/ in your browser.
107
+
108
+ HTTP/2 requires HTTPS to work, therefore in development mode,
109
+ Mayu will use the [localhost](https://github.com/socketry/localhost) gem
110
+ to generate a self-signed certificate for localhost.
111
+
112
+ Depending on your system/browser you might need to do one of the following:
113
+
114
+ <details>
115
+ <summary><strong>MacOS:</strong> Add the certificate to the keychain</summary>
116
+ <blockquote>
117
+ <ol>
118
+ <li>Open <code>~/.localhost/localhost.crt</code> with Keychain Access.</li>
119
+ <li>Choose <i>Get Info</i> and open <i>Trust</i> then choose `Always trust`.</li>
120
+ <li>Restart your browsers.</li>
121
+ </ol>
122
+ </blockquote>
123
+ </details>
124
+
125
+ <details>
126
+ <summary><strong>Chrome:</strong> Enable self-signed certs for localhost</summary>
127
+ <blockquote>
128
+ Go to <code>chrome://flags/#allow-insecure-localhost</code> and enable the setting.
129
+ This will allow requests to localhost over HTTPS even when an invalid
130
+ certificate is presented.
131
+ </blockquote>
132
+ </details>
133
+
134
+ <details>
135
+ <summary><strong>Firefox:</strong> Add an exception for the certificate</summary>
136
+ <blockquote>
137
+ Firefox will show <strong>Warning: Potential Security Risk Ahead</strong>.
138
+ Click <i>Advanced</i>, then <i>Accept the Risk and Continue</i> to add an exception
139
+ for this certificate.
140
+ </blockquote>
141
+ </details>
142
+
143
+ ## Run the tests
144
+
145
+ rake test
146
+
147
+ # Features
148
+
149
+ Most of these features are implemented, however, there are lots of sneaky bugs.
150
+ Contributions in such as bug reports or pull requests are appreciated!
151
+ :green_heart:
152
+
153
+ The [example app](https://github.com/mayu-live/framework/blob/main/example/)
154
+ is deployed to [`https://mayu.live/`](https://mayu.live/) as a proof of concept.
155
+
156
+ ## 100% server side
157
+
158
+ Mayu keeps all state on the server and all HTML is being rendered on the server.
159
+
160
+ There is no need to implement an API, you access databases
161
+ and private APIs directly in your callback handlers.
162
+
163
+ Mayu detects changes in components and sends instructions
164
+ on how to patch the DOM to the browser using the
165
+ [Streams API](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API).
166
+ [Client stream implementation](https://github.com/mayu-live/framework/blob/main/lib/mayu/client/src/stream.ts).
167
+
168
+ Callbacks are regular `POST`-requests to
169
+ `/__mayu/session/#{session_id}/#{callback_id}`,
170
+ where the body contains the
171
+ [serialized event data](https://github.com/mayu-live/framework/blob/main/lib/mayu/client/src/serializeEvent.ts).
172
+
173
+ ## 100% async
174
+
175
+ [socketry/async](https://github.com/socketry/async) makes it possible
176
+ to do all this without blocking.
177
+
178
+ ### `app/components/Clock.haml`
179
+
180
+ ```haml
181
+ :ruby
182
+ mount do
183
+ loop do
184
+ update(time: Time.now.to_s)
185
+ sleep 0.5
186
+ end
187
+ end
188
+ %p= state[:time]
189
+ ```
190
+
191
+ This will print the current server time.
192
+
193
+ The component will render once every second even though it updates
194
+ twice per second, since the time string only changes once per second.
195
+
196
+ [Analog SVG clock component](https://github.com/mayu-live/framework/blob/main/example/app/components/Clock.haml)
197
+
198
+ ### Async loading
199
+
200
+ It's also easy to defer rendering until some action has happened,
201
+ for example, the [form demo](https://mayu.live/demos/form) loads
202
+ tab content asynchronously when the mouse enters the tab header,
203
+ so when the user clicks the tab, the content is already loaded.
204
+
205
+ [Tabs implementation](https://github.com/mayu-live/framework/blob/main/example/app/components/Layout/Tabs.haml)
206
+
207
+ ## Components
208
+
209
+ Components are the building blocks of a Mayu application.
210
+ They contain logic and return other components or HTML elements.
211
+
212
+ You might be familiar with ReactJS and other component based
213
+ rendering libraries. This is the same thing, but in Ruby.
214
+
215
+ ## Scoped CSS
216
+
217
+ All class names and element names are given an unique name,
218
+ inspired by [CSS Modules](https://github.com/css-modules/css-modules).
219
+
220
+ Class names are applied automatically by Haml.
221
+
222
+ ### `app/components/Example.haml`
223
+
224
+ ```haml
225
+ :css
226
+ .box {
227
+ padding: 1px;
228
+ border: 1px solid #000;
229
+ }
230
+
231
+ .hello {
232
+ font-weight: bold;
233
+ }
234
+
235
+ button {
236
+ background: #0f0;
237
+ color: #fff;
238
+ }
239
+ .box
240
+ %p.hello Hello world
241
+ %button Click me!
242
+ ```
243
+
244
+ This would generate the following HTML:
245
+
246
+ ```html
247
+ <div class="/app/components/Example.box?MjQSEK">
248
+ <p class="/app/components/Example.hello?MjQSEK">Hello world</p>
249
+ <button class="/app/components/Example_button?MjQSEK">Click me!</button>
250
+ </div>
251
+ ```
252
+
253
+ Those are valid class names, as long as the characters are escaped in the
254
+ CSS-file. [Specification](https://www.w3.org/TR/css-syntax-3/#consume-name).
255
+
256
+ This will be inserted into `<head>`:
257
+
258
+ ```html
259
+ <link
260
+ rel="stylesheet"
261
+ href="/__mayu/static/NtXGjOdgHqDJUnAhmk3NwuzFnkk8Z1NlBCE_XykVE-8=.css"
262
+ />
263
+ ```
264
+
265
+ The browser will also be made aware of the assets used on a page via the
266
+ [Link](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link)-header,
267
+ so that they can load even before the browser starts parsing the HTML.
268
+
269
+ $ curl -i https://localhost:9292 -k
270
+ HTTP/2 200
271
+ content-length: 5260
272
+ content-type: text/html; charset=utf-8
273
+ link: </__mayu/static/jkA-D11H90ChVHYqIOKn8I_A4w2MJ4nG-UEVP19UGqg=.js>; rel=preload; as=script; crossorigin=anonymous; fetchpriority=high, </__mayu/static/NtXGjOdgHqDJUnAhmk3NwuzFnkk8Z1NlBCE_XykVE-8=.css>; rel=preload; as=style, </__mayu/static/u6rK2NKHRcFKribL1sMcDdr1gXHbgYIVznfN5RJEKCA=.css>; rel=preload; as=style, </__mayu/static/shJPApqH5hptQERL4DivMTX42leUQRht9vGW4X_Rr84=.css>; rel=preload; as=style, </__mayu/static/ZStAGN7uGe7CU3cxSgAIOL550d1VDqVDUzdiQuFOXo8=.css>; rel=preload; as=style
274
+
275
+ ### Separate CSS-files
276
+
277
+ If you have very complex CSS, or maybe generate CSS-files,
278
+ you can create a `.css`-file right next to your component.
279
+
280
+ This does the same thing as the previous example:
281
+
282
+ #### `app/components/Example.haml`
283
+
284
+ ```css
285
+ .box {
286
+ padding: 1px;
287
+ border: 1px solid #000;
288
+ }
289
+
290
+ .hello {
291
+ font-weight: bold;
292
+ }
293
+
294
+ .button {
295
+ background: #0f0;
296
+ color: #fff;
297
+ }
298
+ ```
299
+
300
+ #### `app/components/Example.haml`
301
+
302
+ ```haml
303
+ .box
304
+ %p.hello Hello world
305
+ %button.button Click me!
306
+ ```
307
+
308
+ You can also mix the two styles.
309
+
310
+ ### Source maps
311
+
312
+ You can debug the sources of your CSS using source maps.
313
+
314
+ ![Source maps screenshot](https://user-images.githubusercontent.com/41148/199131855-d6159b68-649c-4c7a-baf1-e1ad2c9bd281.png)
315
+
316
+ ## State management
317
+
318
+ Mayu comes with some basic state management inspired by
319
+ [Redux Toolkit](https://redux-toolkit.js.org/).
320
+
321
+ This is implemented but not yet integrated into the VDOM logic.
322
+
323
+ [Example store](https://github.com/mayu-live/prototype/blob/main/example/store/auth.rb)
324
+
325
+ Ideally I would want something like [XState](https://xstate.js.org/),
326
+ but I'm not experienced with it so I can't make anything like it.
327
+
328
+ ## Path based routing
329
+
330
+ Routing is inspired by the
331
+ [Next.js Layouts RFC](https://nextjs.org/blog/layouts-rfc).
332
+
333
+ It's a simple and straight forward approach, and it's super
334
+ easy to locate files using a fuzzy finder plugin.
335
+
336
+ Here's the structure of a blog app:
337
+
338
+ ```
339
+ app
340
+ ├── root.haml
341
+ ├── root.css
342
+ └── pages
343
+ ├── page.haml
344
+ ├── layout.haml
345
+ ├── layout.css
346
+ ├── about
347
+ │   ├── page.haml
348
+ │   └── page.css
349
+ └── posts
350
+ ├── page.haml
351
+ ├── layout.haml
352
+ └── :id
353
+ └── page.haml
354
+ ```
355
+
356
+ This would create the following routes:
357
+
358
+ | **path** | **component** | **layouts** |
359
+ | ------------- | -------------------------------- | ----------------------------------------------------- |
360
+ | `/` | `app/pages/page.haml` | `app/pages/layout.haml` |
361
+ | `/about/` | `app/pages/about/page.haml` | `app/pages/layout.haml` |
362
+ | `/posts/` | `app/pages/posts/page.haml` | `app/pages/layout.haml` `app/pages/posts/layout.haml` |
363
+ | `/posts/:id/` | `app/pages/posts/[id]/page.haml` | `app/pages/layout.haml` `app/pages/posts/layout.haml` |
364
+ | `/*` | `app/pages/404.haml` | `app/pages/layout.haml` |
365
+
366
+ For a real-world example, check out
367
+ [`example/app/pages/`](https://github.com/mayu-live/framework/tree/main/example/app/pages).
368
+
369
+ ## Hot reloading
370
+
371
+ There is a resource system inspired by JavaScript bundlers that loads all
372
+ types of files.
373
+
374
+ ### Development mode
375
+
376
+ Components and styles update immediately in the browser as you edit files.
377
+ No browser refresh needed.
378
+
379
+ ## Production mode
380
+
381
+ Before a server shuts down (when receiving the `SIGINT` signal),
382
+ it will pause all sessions, serialize and encrypt them, and send
383
+ them to the client and close the connection.
384
+
385
+ The client will then reconnect and post the encrypted session
386
+ which will be decrypted, verified, deserialized and resumed.
387
+
388
+ I don't know how stable this is at the moment.
389
+ Sometimes it seems like it can't restore components properly,
390
+ maybe when their implementation has changed.
391
+
392
+ Whenever [Issue #20](https://github.com/mayu-live/framework/issues/20)
393
+ has been fixed, it would be quite easy to serialize the browser DOM and
394
+ send it along with the encrypted state when resuming, and then use a
395
+ DOM-diffing algorithm (like [morphdom](https://github.com/patrick-steele-idem/morphdom))
396
+ on the browser DOM vs the DOM generated by the VDOM.
397
+
398
+ ## Optimized data transfer
399
+
400
+ Everything is minified and optimized and deliviered over HTTP/2.
401
+ Images are scaled into different versions, non-binary assets are
402
+ compressed with Brotli.
403
+
404
+ Asset filenames are based on their content hash so that they can
405
+ be cached easily without having to worry about expiring them when
406
+ they change.
407
+
408
+ The message stream uses [DecompressionStream](https://wicg.github.io/compression/#decompression-stream)
409
+ with the [`deflate-raw`](https://wicg.github.io/compression/#supported-formats)
410
+ format. Browsers that don't support DecompressionStream will download a
411
+ replacement based on [fflate](https://github.com/101arrowz/fflate).
412
+
413
+ Messages are packed with [MessagePack](https://msgpack.org/index.html),
414
+ which is supposed to be very efficient, although it's also the largest
415
+ dependency at the moment. A good thing with MessagePack is that it
416
+ can send binary data, which is useful when transferring state.
417
+
418
+ First page load with Slow 3G throttling (no cache):
419
+
420
+ ![Request waterfall](https://user-images.githubusercontent.com/41148/198865376-5382a538-44a3-4058-8ba6-6d178cc78b37.png)
421
+
422
+ Second page load with Slow 3G throttling (cache):
423
+
424
+ ![Request waterfall](https://user-images.githubusercontent.com/41148/198865399-d4d428ec-89c6-4469-bec1-964040c41c2c.png)
425
+
426
+ ## Realtime metrics
427
+
428
+ Mayu exposes a [Prometheus](https://prometheus.io/)-endpoint for metrics so you can see how your app performs.
429
+
430
+ Screenshots from [Grafana on Fly.io](https://fly.io/docs/reference/metrics/#managed-grafana-preview).
431
+
432
+ ![Active sessions](https://user-images.githubusercontent.com/41148/193404404-9018c9d9-e575-48db-8845-3f56ced0c16f.png)
433
+ ![Patch times and counts](https://user-images.githubusercontent.com/41148/193398411-cc5bf2d6-d353-42eb-bcf5-ccc1feb7099a.png)
434
+
435
+ ## Haml
436
+
437
+ Mayu uses [Haml](https://haml.info/), it's pretty convenient.
438
+
439
+ Check out the [Haml Reference](https://haml.info/docs/yardoc/file.reference.html).
440
+ Mayu has some differences with regular Haml to make it work better with a virtual DOM,
441
+ [you can read more about that in the documentation](https://mayu.live/docs/components).
442
+
443
+ Look at this example:
444
+
445
+ [`./example/app/pages/Counter.haml`](https://github.com/mayu-live/framework/blob/main/example/app/pages/Counter.haml)
446
+
447
+ That above code will be transformed into something like this:
448
+
449
+ ```ruby
450
+ # frozen_string_literal: true
451
+ Self =
452
+ setup_component(
453
+ assets: ["0tyaKLqdvUGGcwZkdPOdMiMoMZoO74sMmtyRTuksjaQ=.css"],
454
+ styles: {
455
+ __Card: "example/app/pages/Counter_Card?7d89edff",
456
+ __article: "example/app/pages/Counter_article?7d89edff",
457
+ __output: "example/app/pages/Counter_output?7d89edff",
458
+ __button: "example/app/pages/Counter_button?7d89edff",
459
+ },
460
+ )
461
+ begin
462
+ Card = import("/app/components/UI/Card")
463
+ def self.get_initial_state(initial_value: 0, **) = { count: initial_value }
464
+ def decrement_disabled = state[:count].zero?
465
+ def handle_decrement
466
+ update do |state|
467
+ count = [0, state[:count] - 1].max
468
+ { count: }
469
+ end
470
+ end
471
+ def handle_increment
472
+ update do |state|
473
+ count = state[:count] + 1
474
+ { count: }
475
+ end
476
+ end
477
+ end
478
+ public def render
479
+ Mayu::VDOM::H[
480
+ Card,
481
+ Mayu::VDOM::H[
482
+ :article,
483
+ Mayu::VDOM::H[
484
+ :button,
485
+ "-",
486
+ **mayu.merge_props(
487
+ { class: :__button },
488
+ { title: "Decrement" },
489
+ {
490
+ onclick: mayu.handler(:handle_decrement),
491
+ disabled: decrement_disabled,
492
+ },
493
+ )
494
+ ],
495
+ Mayu::VDOM::H[
496
+ :output,
497
+ state[:count],
498
+ **mayu.merge_props({ class: :__output })
499
+ ],
500
+ Mayu::VDOM::H[
501
+ :button,
502
+ "+",
503
+ **mayu.merge_props(
504
+ { class: :__button },
505
+ { title: "Increment" },
506
+ { onclick: mayu.handler(:handle_increment) },
507
+ )
508
+ ],
509
+ **mayu.merge_props({ class: :__article })
510
+ ],
511
+ **mayu.merge_props({ class: :__Card }, { class: :card })
512
+ ]
513
+ end
514
+ ```
515
+
516
+ [Check out more examples in the tests](https://github.com/mayu-live/framework/blob/main/lib/mayu/resources/transformers/haml.test.rb)
517
+
518
+ # Implementation notes
519
+
520
+ ## Tests
521
+
522
+ Tests are located in the `lib/`-directory next to their implementation.
523
+ So for `lib/mayu/state.rb` the test would be located in
524
+ `lib/mayu/state.test.rb`.
525
+
526
+ This pattern is quite common in JavaScript
527
+ ([Jest does this](https://jestjs.io/docs/configuration#testmatch-arraystring)),
528
+ and it's quite convenient to have things that are related close to each other,
529
+ rather than to have a separate tree for tests.
530
+
531
+ It's also preferred to test things on a higher level, and only write unit
532
+ tests for specific edge cases and trickier situations.
533
+ [Sorbet](https://sorbet.org/) is pretty good at finding errors.
534
+ If the higher level tests pass, then everything works as expected.
535
+
536
+ The example app could also be considered to be a test.
537
+ It should always work and be updated to use the latest features.
538
+
539
+ ## Virtual DOM
540
+
541
+ Components return a `VDOM::Descriptor` which has a `type`, `props` and a `key`,
542
+ similar to React, and `props` can also contain children.
543
+ `VDOM::VTree` is responsible for keeping track of the `VDOM::VNode`s that make
544
+ up the application. A `VNode` has a `Descriptor` and children which is an array
545
+ of `VNode` objects. It can also have a component, in that case it would call
546
+ the appropriate lifecycle methods of that component and pass its descriptors'
547
+ props to the component before rendering.
548
+
549
+ The child diffing algorithm is quite inefficient. I have tried to implement
550
+ the algorithm in snabbdom/preact/million several times, but they rely
551
+ on DOM-operations for ordering (`node.insertBefore`) and the algorithm has
552
+ to take care of that and make sure that the order is exactly the same in the
553
+ VDOM as in the DOM after all patch operations have been applied.
554
+
555
+ The child diffing algorithm makes a few unnecessary moves, and there's lots of
556
+ room for improvement, but at least the order is correct.
557
+
558
+ ## Server
559
+
560
+ The server is configured in [`mayu.toml`](https://github.com/mayu-live/framework/blob/main/example/mayu.toml)
561
+ in the project root.
562
+
563
+ ### Development
564
+
565
+ For development you probably want these settings:
566
+
567
+ ```toml
568
+ [dev.server]
569
+ count = 1
570
+ hot_swap = true
571
+ self_signed_cert = true
572
+ ```
573
+
574
+ ### Production
575
+
576
+ The production server depends on the output from a build step that
577
+ parses all inputs and generates static files.
578
+
579
+ ```toml
580
+ [dev.server]
581
+ hot_swap = false
582
+ self_signed_cert = false
583
+ ```
584
+
585
+ ## Static typing
586
+
587
+ Most files are strictly typed with [Sorbet](https://sorbet.org/).
588
+
589
+ Some aren't strictly typed yet, but the goal is to enable
590
+ strict typechecking everywhere.
591
+
592
+ # Contributing
593
+
594
+ Bug reports and pull requests are welcome on GitHub at
595
+ https://github.com/mayu-live/framework.
596
+ This project is intended to be a safe, welcoming space for collaboration,
597
+ and contributors are expected to adhere to the
598
+ [code of conduct](https://github.com/mayu-live/framework/blob/main/CODE_OF_CONDUCT.md).
data/exe/mayu ADDED
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "sorbet-runtime"
4
+
5
+ MAYU_ROOT = File.join(File.dirname(__FILE__), "..")
6
+ $LOAD_PATH.unshift(File.join(MAYU_ROOT, "..", "..", "lib"))
7
+
8
+ if ARGV.include?("--disable-sorbet")
9
+ puts "\e[1mDisabling sorbet\e[0m"
10
+ require "mayu/disable_sorbet"
11
+ Mayu::DisableSorbet.disable_sorbet!
12
+ else
13
+ puts "\e[2mDisable sorbet with --disable-sorbet\e[0m"
14
+ end
15
+
16
+ if RubyVM.const_defined?(:YJIT)
17
+ if RubyVM::YJIT.enabled?
18
+ puts "\e[1mYJIT is enabled!\e[0m"
19
+ else
20
+ puts "\e[2mYJIT is disabled!\e[0m"
21
+ end
22
+ else
23
+ puts "\e[2mYJIT is not supported!\e[0m"
24
+ end
25
+
26
+ if RubyVM::MJIT.enabled?
27
+ puts "\e[1mMJIT is enabled!\e[0m"
28
+ end
29
+
30
+ require "mayu/banner"
31
+ require "mayu/commands"
32
+
33
+ Mayu::Commands.call(ARGV)