Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/turn js transform to gleam #14

Merged
merged 10 commits into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
*.ez
/build
erl_crash.dump
.DS_Store
Thumbs.db
23 changes: 23 additions & 0 deletions e2e/server_components/.github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: test

on:
push:
branches:
- master
- main
pull_request:

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: erlef/setup-beam@v1
with:
otp-version: "26.0.2"
gleam-version: "1.0.0"
rebar3-version: "3"
# elixir-version: "1.15.4"
- run: gleam deps download
- run: gleam test
- run: gleam format --check src test
4 changes: 4 additions & 0 deletions e2e/server_components/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
*.beam
*.ez
/build
erl_crash.dump
25 changes: 25 additions & 0 deletions e2e/server_components/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# server_components

[![Package Version](https://img.shields.io/hexpm/v/server_components)](https://hex.pm/packages/server_components)
[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/server_components/)

```sh
gleam add server_components
```
```gleam
import server_components

pub fn main() {
// TODO: An example of the project in use
}
```

Further documentation can be found at <https://hexdocs.pm/server_components>.

## Development

```sh
gleam run # Run the project
gleam test # Run the tests
gleam shell # Run an Erlang shell
```
27 changes: 27 additions & 0 deletions e2e/server_components/gleam.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name = "server_components"
version = "1.0.0"

# Fill out these fields if you intend to generate HTML documentation or publish
# your project to the Hex package manager.
#
# description = ""
# licences = ["Apache-2.0"]
# repository = { type = "github", user = "username", repo = "project" }
# links = [{ title = "Website", href = "https://gleam.run" }]
#
# For a full reference of all the available options, you can have a look at
# https://gleam.run/writing-gleam/gleam-toml/.

[dependencies]
gleam_stdlib = "~> 0.34 or ~> 1.0"
lustre = "~> 4.1"
lustre_dev_tools = "1.1.2"
sketch = {path = "../.."}
gleam_http = "~> 3.6"
mist = "~> 1.0"
gleam_erlang = "~> 0.25"
gleam_json = "~> 1.0"
gleam_otp = "~> 0.10"

[dev-dependencies]
gleeunit = "~> 1.0"
47 changes: 47 additions & 0 deletions e2e/server_components/manifest.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# This file was generated by Gleam
# You typically do not need to edit this file

packages = [
{ name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" },
{ name = "birl", version = "1.6.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "976CFF85D34D50F7775896615A71745FBE0C325E50399787088F941B539A0497" },
{ name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" },
{ name = "gleam_community_ansi", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "FE79E08BF97009729259B6357EC058315B6FBB916FAD1C2FF9355115FEB0D3A4" },
{ name = "gleam_community_colour", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "795964217EBEDB3DA656F5EB8F67D7AD22872EB95182042D3E7AFEF32D3FD2FE" },
{ name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" },
{ name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" },
{ name = "gleam_javascript", version = "0.8.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "14D5B7E1A70681E0776BF0A0357F575B822167960C844D3D3FA114D3A75F05A8" },
{ name = "gleam_json", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "8B197DD5D578EA6AC2C0D4BDC634C71A5BCA8E7DB5F47091C263ECB411A60DF3" },
{ name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" },
{ name = "gleam_package_interface", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_package_interface", source = "hex", outer_checksum = "52A721BCA972C8099BB881195D821AAA64B9F2655BECC102165D5A1097731F01" },
{ name = "gleam_stdlib", version = "0.36.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "C0D14D807FEC6F8A08A7C9EF8DFDE6AE5C10E40E21325B2B29365965D82EB3D4" },
{ name = "glearray", version = "0.2.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glearray", source = "hex", outer_checksum = "908154F695D330E06A37FAB2C04119E8F315D643206F8F32B6A6C14A8709FFF4" },
{ name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" },
{ name = "glint", version = "0.18.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "BB0F14643CC51C069A5DC6E9082EAFCD9967AFD1C9CC408803D1A40A3FD43B54" },
{ name = "glisten", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "glisten", source = "hex", outer_checksum = "CF3A9383E9BA4A8CBAF2F7B799716290D02F2AC34E7A77556B49376B662B9314" },
{ name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" },
{ name = "logging", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "82C112ED9B6C30C1772A6FE2613B94B13F62EA35F5869A2630D13948D297BD39" },
{ name = "lustre", version = "4.1.7", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], otp_app = "lustre", source = "hex", outer_checksum = "64F5D7E4DF51280185F70296ACB7D3DCC9B5DA09EFC5257F0E5601846DFBEF23" },
{ name = "lustre_dev_tools", version = "1.1.2", build_tools = ["gleam"], requirements = ["argv", "filepath", "gleam_community_ansi", "gleam_erlang", "gleam_json", "gleam_otp", "gleam_package_interface", "gleam_stdlib", "glint", "simplifile", "spinner", "tom"], otp_app = "lustre_dev_tools", source = "hex", outer_checksum = "E49220E4D43D059C52BEB4FD878CCE694829DBE9DF1CCE5487C5607D390590F7" },
{ name = "mist", version = "1.0.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "7765E53DCC9ACCACF217B8E0CA3DE7E848C783BFAE5118B75011E81C2C80385C" },
{ name = "plinth", version = "0.2.0", build_tools = ["gleam"], requirements = ["gleam_javascript", "gleam_json", "gleam_stdlib"], otp_app = "plinth", source = "hex", outer_checksum = "83211E672D83F3CE14681D0ECD3AD883EE7588E423E7C9DDDB460014AD60AC24" },
{ name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" },
{ name = "repeatedly", version = "2.1.1", build_tools = ["gleam"], requirements = [], otp_app = "repeatedly", source = "hex", outer_checksum = "38808C3EC382B0CD981336D5879C24ECB37FCB9C1D1BD128F7A80B0F74404D79" },
{ name = "simplifile", version = "1.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "1D5DFA3A2F9319EC85825F6ED88B8E449F381B0D55A62F5E61424E748E7DDEB0" },
{ name = "sketch", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "lustre", "plinth"], source = "local", path = "../.." },
{ name = "snag", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "54D32E16E33655346AA3E66CBA7E191DE0A8793D2C05284E3EFB90AD2CE92BCC" },
{ name = "spinner", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_erlang", "gleam_stdlib", "glearray", "repeatedly"], otp_app = "spinner", source = "hex", outer_checksum = "200BA3D4A04D468898E63C0D316E23F526E02514BC46454091975CB5BAE41E8F" },
{ name = "thoas", version = "0.4.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "4918D50026C073C4AB1388437132C77A6F6F7C8AC43C60C13758CC0ADCE2134E" },
{ name = "tom", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "0831C73E45405A2153091226BF98FB485ED16376988602CC01A5FD086B82D577" },
]

[requirements]
gleam_erlang = { version = "~> 0.25" }
gleam_http = { version = "~> 3.6" }
gleam_json = { version = "~> 1.0" }
gleam_otp = { version = "~> 0.10"}
gleam_stdlib = { version = "~> 0.34 or ~> 1.0" }
gleeunit = { version = "~> 1.0" }
lustre = { version = "~> 4.1" }
lustre_dev_tools = { version = "1.1.2" }
mist = { version = "~> 1.0" }
sketch = { path = "../.." }
92 changes: 92 additions & 0 deletions e2e/server_components/src/counter.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import gleam/int
import gleam/result
import lustre
import lustre/element/html
import lustre/event
import sketch
import sketch/lustre as sketch_lustre
import sketch/media
import sketch/options as sketch_options
import sketch/size.{px}

pub type Model =
Int

pub type Msg {
Increment
Decrement
}

pub fn main() {
let init = fn(_) { 0 }
let assert Ok(cache) =
sketch_options.node()
|> sketch_lustre.setup()

let assert Ok(_) =
view
|> sketch_lustre.compose(cache)
|> lustre.simple(init, update, _)
|> lustre.start("#app", Nil)
}

pub fn app() {
let init = fn(_) { 0 }
sketch_options.node()
|> sketch_lustre.setup()
|> result.map(fn(cache) {
view
|> sketch_lustre.compose(cache)
|> lustre.simple(init, update, _)
})
}

fn update(model: Model, msg: Msg) {
case msg {
Increment -> model + 1
Decrement -> model - 1
}
}

fn main_class() {
sketch.class([
sketch.background("red"),
sketch.display("flex"),
sketch.flex_direction("row"),
sketch.gap(px(12)),
sketch.padding(px(12)),
sketch.hover([sketch.background("yellow")]),
sketch.media(media.max_width(px(450)), [
sketch.background("purple"),
sketch.hover([sketch.background("white")]),
]),
])
|> sketch.to_lustre()
}

fn color_class(model: Model) {
let back = case model % 3 {
0 -> "blue"
_ -> "green"
}
let id = "color-" <> back
sketch.dynamic(id, [sketch.background(back)])
|> sketch.to_lustre()
}

fn button_class() {
sketch.class([sketch.cursor("crosshair"), sketch.font_size_("14px")])
|> sketch.to_lustre()
}

fn view(model: Model) {
html.div([main_class()], [
html.button([event.on_click(Decrement), button_class()], [
html.text("Decrement"),
]),
html.div([color_class(model)], [html.text(int.to_string(model))]),
html.button([event.on_click(Increment), button_class()], [
html.text("Increment"),
]),
])
}
165 changes: 165 additions & 0 deletions e2e/server_components/src/server_components.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import counter
import gleam/bytes_builder
import gleam/erlang
import gleam/erlang/process.{type Selector, type Subject}
import gleam/http/request.{type Request}
import gleam/http/response.{type Response}
import gleam/json
import gleam/option.{type Option, None}
import gleam/otp/actor
import gleam/result
import lustre
import lustre/attribute
import lustre/element
import lustre/element/html.{html}
import lustre/server_component
import mist.{
type Connection, type ResponseData, type WebsocketConnection,
type WebsocketMessage,
}

pub fn main() {
let assert Ok(_) =
fn(req: Request(Connection)) -> Response(ResponseData) {
case request.path_segments(req) {
// Set up the websocket connection to the client. This is how we send
// DOM updates to the browser and receive events from the client.
["counter"] ->
mist.websocket(
request: req,
on_init: socket_init,
on_close: socket_close,
handler: socket_update,
)

// We need to serve the server component runtime. There's also a minified
// version of this script for production.
["lustre-server-component.mjs"] -> {
let assert Ok(priv) = erlang.priv_directory("lustre")
let path = priv <> "/static/lustre-server-component.mjs"

mist.send_file(path, offset: 0, limit: None)
|> result.map(fn(script) {
response.new(200)
|> response.prepend_header("content-type", "application/javascript")
|> response.set_body(script)
})
|> result.lazy_unwrap(fn() {
response.new(404)
|> response.set_body(mist.Bytes(bytes_builder.new()))
})
}

// For all other requests we'll just serve some HTML that renders the
// server component.
_ ->
response.new(200)
|> response.prepend_header("content-type", "text/html")
|> response.set_body(
html([], [
html.head([], [
html.link([
attribute.rel("stylesheet"),
attribute.href(
"https://cdn.jsdelivr.net/gh/lustre-labs/ui/priv/styles.css",
),
]),
html.script(
[
attribute.type_("module"),
attribute.src("/lustre-server-component.mjs"),
],
"",
),
]),
html.body([], [
server_component.component([server_component.route("/counter")]),
]),
])
|> element.to_document_string_builder
|> bytes_builder.from_string_builder
|> mist.Bytes,
)
}
}
|> mist.new
|> mist.port(3000)
|> mist.start_http

process.sleep_forever()
}

//

type Counter =
Subject(lustre.Action(counter.Msg, lustre.ServerComponent))

fn socket_init(
conn: WebsocketConnection,
) -> #(Counter, Option(Selector(lustre.Patch(counter.Msg)))) {
let assert Ok(app) = counter.app()
let assert Ok(counter) = lustre.start_actor(app, 0)

process.send(
counter,
server_component.subscribe(
// server components can have many connected clients, so we need a way to
// identify this client.
"ws",
// this callback is called whenever the server component has a new patch
// to send to the client. here we json encode that patch and send it to
// via the websocket connection.
//
// a more involved version would have us sending the patch to this socket's
// subject, and then it could be handled (perhaps with some other work) in
// the `mist.Custom` branch of `socket_update` below.
fn(patch) {
let _ =
patch
|> server_component.encode_patch
|> json.to_string
|> mist.send_text_frame(conn, _)

Nil
},
),
)

#(
// we store the server component's `Subject` as this socket's state so we
// can shut it down when the socket is closed.
counter,
// the `None` here means we aren't planning on receiving any messages from
// elsewhere and dont need a `Selector` to handle them.
None,
)
}

fn socket_update(
counter: Counter,
_conn: WebsocketConnection,
msg: WebsocketMessage(lustre.Patch(counter.Msg)),
) {
case msg {
mist.Text(json) -> {
// we attempt to decode the incoming text as an action to send to our
// server component runtime.
let action = json.decode(json, server_component.decode_action)

case action {
Ok(action) -> process.send(counter, action)
Error(_) -> Nil
}

actor.continue(counter)
}

mist.Binary(_) -> actor.continue(counter)
mist.Custom(_) -> actor.continue(counter)
mist.Closed | mist.Shutdown -> actor.Stop(process.Normal)
}
}

fn socket_close(counter: Counter) {
process.send(counter, lustre.shutdown())
}
Loading
Loading