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.
- checksums.yaml +7 -0
- data/COPYING +661 -0
- data/README.md +598 -0
- data/exe/mayu +33 -0
- data/lib/mayu/app_metrics.rb +93 -0
- data/lib/mayu/banner.rb +12 -0
- data/lib/mayu/client/README.md +17 -0
- data/lib/mayu/client/dist/DecompressionStreamPolyfill-3ceba43e.js +1 -0
- data/lib/mayu/client/dist/DecompressionStreamPolyfill-3ceba43e.js.br +0 -0
- data/lib/mayu/client/dist/DecompressionStreamPolyfill-3ceba43e.js.map +1 -0
- data/lib/mayu/client/dist/DecompressionStreamPolyfill-3ceba43e.js.map.br +0 -0
- data/lib/mayu/client/dist/custom-elements/mayu-alert-cd7ad2a4.js +1 -0
- data/lib/mayu/client/dist/custom-elements/mayu-alert-cd7ad2a4.js.map +1 -0
- data/lib/mayu/client/dist/custom-elements/mayu-disconnected-9f349f46.js +1 -0
- data/lib/mayu/client/dist/custom-elements/mayu-disconnected-9f349f46.js.map +1 -0
- data/lib/mayu/client/dist/custom-elements/mayu-exception-63df4e8c.js +1 -0
- data/lib/mayu/client/dist/custom-elements/mayu-exception-63df4e8c.js.map +1 -0
- data/lib/mayu/client/dist/custom-elements/mayu-ping-c498c2a6.js +1 -0
- data/lib/mayu/client/dist/custom-elements/mayu-ping-c498c2a6.js.map +1 -0
- data/lib/mayu/client/dist/custom-elements/mayu-progress-bar-eb3e1ac8.js +1 -0
- data/lib/mayu/client/dist/custom-elements/mayu-progress-bar-eb3e1ac8.js.map +1 -0
- data/lib/mayu/client/dist/entries.json +3 -0
- data/lib/mayu/client/dist/main-4b49dbc4.js +1 -0
- data/lib/mayu/client/dist/main-4b49dbc4.js.br +0 -0
- data/lib/mayu/client/dist/main-4b49dbc4.js.map +1 -0
- data/lib/mayu/client/dist/main-4b49dbc4.js.map.br +0 -0
- data/lib/mayu/client/package.json +39 -0
- data/lib/mayu/client/rollup.config.js +81 -0
- data/lib/mayu/client/src/DecompressionStream.ts +15 -0
- data/lib/mayu/client/src/DecompressionStreamPolyfill.ts +43 -0
- data/lib/mayu/client/src/MimeTypes.ts +4 -0
- data/lib/mayu/client/src/NodeTree.ts +445 -0
- data/lib/mayu/client/src/custom-elements/mayu-alert.html +137 -0
- data/lib/mayu/client/src/custom-elements/mayu-alert.ts +62 -0
- data/lib/mayu/client/src/custom-elements/mayu-disconnected.html +134 -0
- data/lib/mayu/client/src/custom-elements/mayu-disconnected.ts +51 -0
- data/lib/mayu/client/src/custom-elements/mayu-exception.html +79 -0
- data/lib/mayu/client/src/custom-elements/mayu-exception.ts +28 -0
- data/lib/mayu/client/src/custom-elements/mayu-log.html +70 -0
- data/lib/mayu/client/src/custom-elements/mayu-log.ts +42 -0
- data/lib/mayu/client/src/custom-elements/mayu-ping.html +36 -0
- data/lib/mayu/client/src/custom-elements/mayu-ping.ts +53 -0
- data/lib/mayu/client/src/custom-elements/mayu-progress-bar.html +44 -0
- data/lib/mayu/client/src/custom-elements/mayu-progress-bar.ts +40 -0
- data/lib/mayu/client/src/custom-elements/types.d.ts +4 -0
- data/lib/mayu/client/src/global.d.ts +26 -0
- data/lib/mayu/client/src/h.ts +27 -0
- data/lib/mayu/client/src/logger.ts +56 -0
- data/lib/mayu/client/src/main.ts +271 -0
- data/lib/mayu/client/src/serializeEvent.ts +90 -0
- data/lib/mayu/client/src/stream.ts +175 -0
- data/lib/mayu/client/src/types.ts +1 -0
- data/lib/mayu/client/src/utils.ts +71 -0
- data/lib/mayu/client/tsconfig.json +18 -0
- data/lib/mayu/colors.rb +34 -0
- data/lib/mayu/commands/base.rb +22 -0
- data/lib/mayu/commands/build.rb +82 -0
- data/lib/mayu/commands.rb +53 -0
- data/lib/mayu/component/base.rb +177 -0
- data/lib/mayu/component/handler_ref.rb +99 -0
- data/lib/mayu/component/helpers.rb +93 -0
- data/lib/mayu/component/interface.rb +18 -0
- data/lib/mayu/component/wrapper.rb +165 -0
- data/lib/mayu/component.rb +54 -0
- data/lib/mayu/configuration.rb +195 -0
- data/lib/mayu/disable_sorbet.rb +23 -0
- data/lib/mayu/environment.rb +151 -0
- data/lib/mayu/event_stream.rb +158 -0
- data/lib/mayu/fetch.rb +88 -0
- data/lib/mayu/html.rb +53 -0
- data/lib/mayu/html.yaml +767 -0
- data/lib/mayu/message_cipher.rb +172 -0
- data/lib/mayu/message_cipher.test.rb +16 -0
- data/lib/mayu/metrics/collector.rb +161 -0
- data/lib/mayu/metrics/exporter.rb +47 -0
- data/lib/mayu/metrics/reporter.rb +187 -0
- data/lib/mayu/metrics.rb +82 -0
- data/lib/mayu/ref_counter.rb +57 -0
- data/lib/mayu/resources/README.md +14 -0
- data/lib/mayu/resources/asset.rb +71 -0
- data/lib/mayu/resources/assets.rb +76 -0
- data/lib/mayu/resources/dependency_graph.rb +306 -0
- data/lib/mayu/resources/dot_exporter.rb +167 -0
- data/lib/mayu/resources/generators/base.rb +18 -0
- data/lib/mayu/resources/generators/copy_file.rb +26 -0
- data/lib/mayu/resources/generators/image.rb +106 -0
- data/lib/mayu/resources/generators/write_file.rb +39 -0
- data/lib/mayu/resources/hot_swap/file_watcher.rb +69 -0
- data/lib/mayu/resources/hot_swap.rb +46 -0
- data/lib/mayu/resources/mermaid_exporter.rb +210 -0
- data/lib/mayu/resources/registry.rb +190 -0
- data/lib/mayu/resources/resolver/base.rb +32 -0
- data/lib/mayu/resources/resolver/filesystem.rb +94 -0
- data/lib/mayu/resources/resolver/static.rb +27 -0
- data/lib/mayu/resources/resolver.rb +13 -0
- data/lib/mayu/resources/resource.rb +150 -0
- data/lib/mayu/resources/transformers/__test__/css/adjacent_selectors.in.css +3 -0
- data/lib/mayu/resources/transformers/__test__/css/adjacent_selectors.out.css +6 -0
- data/lib/mayu/resources/transformers/__test__/css/attributes.in.css +3 -0
- data/lib/mayu/resources/transformers/__test__/css/attributes.out.css +6 -0
- data/lib/mayu/resources/transformers/__test__/css/composes.in.css +6 -0
- data/lib/mayu/resources/transformers/__test__/css/composes.out.css +10 -0
- data/lib/mayu/resources/transformers/__test__/css/element_selectors.in.css +3 -0
- data/lib/mayu/resources/transformers/__test__/css/element_selectors.out.css +6 -0
- data/lib/mayu/resources/transformers/__test__/css/has.in.css +7 -0
- data/lib/mayu/resources/transformers/__test__/css/has.out.css +10 -0
- data/lib/mayu/resources/transformers/__test__/css/media_queries.in.css +8 -0
- data/lib/mayu/resources/transformers/__test__/css/media_queries.out.css +12 -0
- data/lib/mayu/resources/transformers/__test__/css/pseudo_classes.in.css +5 -0
- data/lib/mayu/resources/transformers/__test__/css/pseudo_classes.out.css +6 -0
- data/lib/mayu/resources/transformers/__test__/haml/README.md +10 -0
- data/lib/mayu/resources/transformers/__test__/haml/case.haml +8 -0
- data/lib/mayu/resources/transformers/__test__/haml/case.rb +15 -0
- data/lib/mayu/resources/transformers/__test__/haml/class_names.haml +13 -0
- data/lib/mayu/resources/transformers/__test__/haml/class_names.rb +26 -0
- data/lib/mayu/resources/transformers/__test__/haml/comments.haml +5 -0
- data/lib/mayu/resources/transformers/__test__/haml/comments.rb +5 -0
- data/lib/mayu/resources/transformers/__test__/haml/css.haml +3 -0
- data/lib/mayu/resources/transformers/__test__/haml/css.rb +11 -0
- data/lib/mayu/resources/transformers/__test__/haml/dashes.haml +3 -0
- data/lib/mayu/resources/transformers/__test__/haml/dashes.rb +11 -0
- data/lib/mayu/resources/transformers/__test__/haml/early_return.haml +4 -0
- data/lib/mayu/resources/transformers/__test__/haml/early_return.rb +9 -0
- data/lib/mayu/resources/transformers/__test__/haml/early_return2.haml +3 -0
- data/lib/mayu/resources/transformers/__test__/haml/early_return2.rb +6 -0
- data/lib/mayu/resources/transformers/__test__/haml/handlers.haml +6 -0
- data/lib/mayu/resources/transformers/__test__/haml/handlers.rb +12 -0
- data/lib/mayu/resources/transformers/__test__/haml/if_else.haml +6 -0
- data/lib/mayu/resources/transformers/__test__/haml/if_else.rb +12 -0
- data/lib/mayu/resources/transformers/__test__/haml/interpolation.haml +8 -0
- data/lib/mayu/resources/transformers/__test__/haml/interpolation.rb +11 -0
- data/lib/mayu/resources/transformers/__test__/haml/object_ref_as_key.haml +1 -0
- data/lib/mayu/resources/transformers/__test__/haml/object_ref_as_key.rb +5 -0
- data/lib/mayu/resources/transformers/__test__/haml/props.haml +4 -0
- data/lib/mayu/resources/transformers/__test__/haml/props.rb +11 -0
- data/lib/mayu/resources/transformers/__test__/haml/slots.haml +5 -0
- data/lib/mayu/resources/transformers/__test__/haml/slots.rb +9 -0
- data/lib/mayu/resources/transformers/__test__/haml/slots_dynamic.haml +3 -0
- data/lib/mayu/resources/transformers/__test__/haml/slots_dynamic.rb +9 -0
- data/lib/mayu/resources/transformers/__test__/haml/slots_fallback.haml +3 -0
- data/lib/mayu/resources/transformers/__test__/haml/slots_fallback.rb +5 -0
- data/lib/mayu/resources/transformers/__test__/haml/spacing.haml +5 -0
- data/lib/mayu/resources/transformers/__test__/haml/spacing.rb +14 -0
- data/lib/mayu/resources/transformers/__test__/haml/spacing2.haml +10 -0
- data/lib/mayu/resources/transformers/__test__/haml/spacing2.rb +11 -0
- data/lib/mayu/resources/transformers/__test__/haml/spacing3.haml +3 -0
- data/lib/mayu/resources/transformers/__test__/haml/spacing3.rb +10 -0
- data/lib/mayu/resources/transformers/css/rouge_lexer.rb +841 -0
- data/lib/mayu/resources/transformers/css.rb +100 -0
- data/lib/mayu/resources/transformers/css.test.rb +87 -0
- data/lib/mayu/resources/transformers/haml.rb +984 -0
- data/lib/mayu/resources/transformers/haml.test.rb +114 -0
- data/lib/mayu/resources/types/README.md +36 -0
- data/lib/mayu/resources/types/base.rb +35 -0
- data/lib/mayu/resources/types/component.rb +198 -0
- data/lib/mayu/resources/types/image.rb +169 -0
- data/lib/mayu/resources/types/javascript.rb +50 -0
- data/lib/mayu/resources/types/nil.rb +23 -0
- data/lib/mayu/resources/types/stylesheet.rb +119 -0
- data/lib/mayu/resources/types/svg.rb +69 -0
- data/lib/mayu/resources/types.rb +37 -0
- data/lib/mayu/routes.rb +170 -0
- data/lib/mayu/routing/builder.rb +108 -0
- data/lib/mayu/routing/matcher.rb +58 -0
- data/lib/mayu/routing/routes.rb +85 -0
- data/lib/mayu/routing.rb +17 -0
- data/lib/mayu/server/app.rb +494 -0
- data/lib/mayu/server/controller.rb +152 -0
- data/lib/mayu/server/errors.rb +110 -0
- data/lib/mayu/server/file_server.rb +140 -0
- data/lib/mayu/server.rb +63 -0
- data/lib/mayu/session.rb +358 -0
- data/lib/mayu/state/README.md +6 -0
- data/lib/mayu/state/action_creator.rb +191 -0
- data/lib/mayu/state/action_wrapper.rb +30 -0
- data/lib/mayu/state/loader.rb +220 -0
- data/lib/mayu/state/store.rb +82 -0
- data/lib/mayu/state.rb +8 -0
- data/lib/mayu/state.test.rb +97 -0
- data/lib/mayu/utils.rb +114 -0
- data/lib/mayu/vdom/children.rb +117 -0
- data/lib/mayu/vdom/component_marshaler.rb +53 -0
- data/lib/mayu/vdom/css_attributes.rb +131 -0
- data/lib/mayu/vdom/descriptor.rb +151 -0
- data/lib/mayu/vdom/descriptor.test.rb +26 -0
- data/lib/mayu/vdom/dom.rb +239 -0
- data/lib/mayu/vdom/h.rb +22 -0
- data/lib/mayu/vdom/id_generator.rb +55 -0
- data/lib/mayu/vdom/interfaces.rb +186 -0
- data/lib/mayu/vdom/marshalling.rb +78 -0
- data/lib/mayu/vdom/reconciliation.rb +205 -0
- data/lib/mayu/vdom/reconciliation.test.rb +56 -0
- data/lib/mayu/vdom/special_elements.rb +108 -0
- data/lib/mayu/vdom/update_context.rb +180 -0
- data/lib/mayu/vdom/vdom.perf.test.rb +146 -0
- data/lib/mayu/vdom/vnode.rb +266 -0
- data/lib/mayu/vdom/vtree.rb +672 -0
- data/lib/mayu/vdom/vtree.test.rb +68 -0
- data/lib/mayu/vdom.rb +8 -0
- data/lib/mayu/vdom.test.rb +73 -0
- data/lib/mayu/version.rb +6 -0
- data/lib/mayu.rb +8 -0
- data/mayu-live.gemspec +70 -0
- 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
|
+
[](https://github.com/mayu-live/framework/actions/workflows/ruby.yml)
|
4
|
+
[](https://github.com/mayu-live/framework/actions/workflows/node.js.yml)
|
5
|
+
[](https://github.com/mayu-live/framework/commits)
|
6
|
+
[](https://github.com/mayu-live/framework/blob/main/COPYING) 
|
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
|
+

|
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
|
+

|
421
|
+
|
422
|
+
Second page load with Slow 3G throttling (cache):
|
423
|
+
|
424
|
+

|
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
|
+

|
433
|
+

|
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)
|