lustre-labs / lustre Goto Github PK
View Code? Open in Web Editor NEWA Gleam web framework for building HTML templates, single page applications, and real-time server components.
Home Page: https://hexdocs.pm/lustre
License: MIT License
A Gleam web framework for building HTML templates, single page applications, and real-time server components.
Home Page: https://hexdocs.pm/lustre
License: MIT License
The lustre/try
preview server at the moment only serves the application from the main
function in the module that corresponds to the project's name.
This is a sensible entry but for folks that want to experiment with an MPA setup it might be nice to detect and serve any routes that setup a Lustre application (or return an Element
see: #28).
This is probably as much scope creep as we should tolerate for now, because ideally we want to push folks into either use vite or esbuild (through the cli) instead.
In other frameworks it's often quite handy to be able to return multiple elements without wrapping them in some single container. Lustre should support this too! We should define a new
variant of the Element
type...
pub opaque type Element(msg) {
...
+ Fragment(List(Element(msg))
...
}
... as well as exposing a new constructor for it.
pub fn fragment(children: List(Element(msg)) -> Element(msg) {
Fragment(children)
}
Then the render methods to_string
and to_string_builder
as well as the runtime need to be updated to support this new element type.
This happens on v3.0.0-rc2
, here's a sample snippet that causes the problem.
element.input([
attribute.on_change(GotUsername),
attribute.value(dynamic.from(state.username)),
]),
We explored this a bit more in the discord thread
Hello!
I wish to insert pre-formatted HTML into my Lustre application.
It is rendered on the server, so I cannot do this with web component tricks, etc.
I could insert placeholder UUIDs and replace them, but this is awkward, error prone, and means I have to render to a string rather than a builder and then traverse and reallocate it once per replacement.
The ideal for me would to be able to insert the HTML string and have Lustre turn off escaping for this fragment. I'd prefer a horrible name to make it clear to the programmer that they should not be using unless they know what they are doing.
html.div([], [
element.dangerously_insert_html_fragment_without_escaping(html_string),
])
Thanks!
Hi!
While there's an effect.map
, there's no effect.flat_map
, which could be useful to handle effect chaining instead of having to rely on the update
chain?
We could need to cascade HTTP calls for example, and it would respect the usual Monad interface 🙂
I can understand it could be an "advanced" feature, but I think it could be nice to open possibilities for development, do you have any opinion on this?
Most frameworks have the concept of a "keyed" node that lets the DOM patching do an in-place update of elements with a stable id even if their position in a list changes. This is important for two reasons:
This was brought up by #79.
I'm not entirely sure if this is a feature we definitely want, but for folks that are dipping their toes into lustre particularly from a backend perspective, they might be less inclined to put something pretty together off the bat.
Having the option to include lustre_ui's default styles (maybe with some additional styling that re-adds things like h1-6
font size etc) will let folks put together things that look semi-reasonable before they commit to lustre and lustre_ui.
This could probably do with some discussion or more thoughts on how/why we'd do this!
It'd be really helpful if the lustre/try
server provided some basic logging. At the very least it should log what host/port the server is running on. Bonus points if it uses ✨ emojis ✨.
Any other logs that might be useful are welcome too!
Steps to reproduce:
cd examples/01-hello-world/
gleam run -m lustre build app
crashes with
Downloading packages
Downloaded 20 packages in 0.03s
Compiling argv
Compiling gleam_stdlib
Compiling filepath
Compiling gleam_community_colour
Compiling gleam_community_ansi
Compiling gleam_erlang
Compiling thoas
===> Analyzing applications...
===> Compiling thoas
Compiling gleam_json
Compiling gleam_otp
Compiling gleam_package_interface
Compiling glearray
Compiling gleeunit
Compiling snag
Compiling glint
warning: Deprecated value used
┌─ /home/grfork/reps/gleam/lustre/examples/01-hello-world/build/packages/glint/src/glint.gleam:493:56
│
493 │ run_and_handle(from: glint, for: args, with: function.constant(Nil))
│ ^^^^^^^^^ This value has been deprecated
It was deprecated with this message: Use a fn literal instead, it is easier
to understand
Compiling justin
Compiling simplifile
Compiling repeatedly
Compiling spinner
Compiling tom
Compiling lustre
Compiling lustre_ui
Compiling app
Compiled in 9.67s
Running lustre.main
⠋ Building your project
warning: Deprecated value used
┌─ /home/grfork/reps/gleam/lustre/examples/01-hello-world/build/packages/glint/src/glint.gleam:493:56
│
493 │ run_and_handle(from: glint, for: args, with: function.constant(Nil))
│ ^^^^^^^^^ This value has been deprecated
It was deprecated with this message: Use a fn literal instead, it is easier
to understand
✅ Project compiled successfully
⠋ Checking if I can bundle your application
warning: Deprecated value used
┌─ /home/grfork/reps/gleam/lustre/examples/01-hello-world/build/packages/glint/src/glint.gleam:493:56
│
493 │ run_and_handle(from: glint, for: args, with: function.constant(Nil))
│ ^^^^^^^^^ This value has been deprecated
It was deprecated with this message: Use a fn literal instead, it is easier
to understand
✅ Esbuild installed!
▲ [WARNING] Import "create_from_string" will always be undefined because there is no matching export in "build/dev/javascript/gleam_erlang/gleam/erlang/atom.mjs" [import-is-undefined]
build/dev/javascript/gleam_erlang/gleam/erlang/process.mjs:78:18:
78 │ let tag = $atom.create_from_string("EXIT");
╵ ~~~~~~~~~~~~~~~~~~
▲ [WARNING] Import "create_from_string" will always be undefined because there is no matching export in "build/dev/javascript/gleam_erlang/gleam/erlang/atom.mjs" [import-is-undefined]
build/dev/javascript/gleam_otp/gleam/otp/actor.mjs:135:10:
135 │ $atom.create_from_string("gleam@otp@actor"),
╵ ~~~~~~~~~~~~~~~~~~
▲ [WARNING] Import "from_string" will always be undefined because there is no matching export in "build/dev/javascript/gleam_erlang/gleam/erlang/charlist.mjs" [import-is-undefined]
build/dev/javascript/gleam_otp/gleam/otp/actor.mjs:169:18:
169 │ $charlist.from_string("Actor discarding unexpected message: ~s"),
╵ ~~~~~~~~~~~
✘ [ERROR] No matching export in "build/dev/javascript/lustre/lustre/cli/utils.mjs" for import "exec"
build/dev/javascript/lustre/lustre/cli/project.mjs:18:9:
18 │ import { exec, map, try$ } from "../../lustre/cli/utils.mjs";
╵ ~~~~
✘ [ERROR] No matching export in "build/dev/javascript/lustre/lustre/cli/utils.mjs" for import "exec"
build/dev/javascript/lustre/lustre/cli/esbuild.mjs:22:9:
22 │ import { exec, keep, replace } from "../../lustre/cli/utils.mjs";
╵ ~~~~
▲ [WARNING] Import "new_subject" will always be undefined because there is no matching export in "build/dev/javascript/gleam_erlang/gleam/erlang/process.mjs" [import-is-undefined]
build/dev/javascript/gleam_otp/gleam/otp/actor.mjs:197:25:
197 │ let subject = $process.new_subject();
╵ ~~~~~~~~~~~
4 of 19 warnings and all 2 errors shown (disable the message limit with --log-limit=0)
❌ Bundling with esbuild
I ran into an error while trying to create a bundle with esbuild:
Doc comments are a bit lacking across the codebase. For a while Lustre's docs over at lustre.build were handwritten separate to this codebase but an open PR on the compiler makes it possible to export a package's documentation as JSON.
Being able to export the docs as JSON means we can use the actual codebase as the source of truth and folks consuming the docs on hex don't get a worse experience. Because we're pushing for another major version bump, now seems like a good time to polish this stuff up and make it good.
Currently the lustre/try
preview server is unable to be configured. Two configuration options we should absolutely support are:
--port
flag that allows the user to change the port the server on.--host
flag to change the servers hostname, This will be useful for serving on 0.0.0.0
.I'm open to more configuration options too, but lustre/try
is intended to be a simple preview server for folks that want to dip their feet into so it shouldn't be too complex.
We could consider using glint but it feels like it might be overkill, perhaps we can come up with our own abstraction that can be used in the cli too.
The first time I run gleam run -m lustre build app
, I get the expected output:
✅ Project compiled successfully
✅ Esbuild installed!
✅ Bundle produced at `./priv/static/taskmgr.mjs`
right after that the second time I run the same command, I get
✅ Project compiled successfully
❌ Checking if I can bundle your application
I couldn't find a public module called `taskmgr` in your project.
The lustre/try
preview server only expects full Lustre applications to be defined by an app's main
function. For the folks super new to Lustre and frontend development in general, even that might be a little too high of a barrier.
We should also support the scenario where they return just a simple Element
, such as:
pub fn main() {
html.h1([], [
element.text("Hello, world!")
])
}
So they can get something on the page ASAP.
Most non-trivial lustre apps will be single-page applications that manage client-side routing. Its common for production backends serving SPAs to serve the app on all get requests so the app can load and take over routing, but currently lustre’s dev server doesnt do this.
This means of you navigate around an app that does some clientside routing and then refresh the page you end up with a 404.
We should add a --spa
flag to lustre dev
so that the app is always served on any route.
Hi, maybe it's a timing issue, as I'm using Gleam v1.0.0, and I see that main
is in the process of being readied for a v4.0.0, but ...
I cloned this repo and then tried to run the first (and second) example, but get the following error:
error: Unknown module
┌─ /home/ian/Projects/github.com/lustre-labs/lustre/src/lustre/cli/project.gleam:10:1
│
10 │ import gleam/package_interface.{type Type, Fn, Named, Tuple, Variable}
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Did you mean `dynamic`?
No module has been found with the name `gleam/package_interface`.
Here's a screenshot after I deleted my checkout, re-cloned, and tried to run again...
Am I doing something wrong? (I'm brand new to Gleam, it looks awesome, and having used Elm in the past, Lustre could very well be my next favourite thing to use for web dev 😄)
The examples folder in wisp is a great example of examples done right: examples are numbered and increase in complexity. You could read these examples in sequence and get a good idea of how wisp works.
Right now Lustre's example folder mostly serves as an ad-hoc test bed to make sure I haven't broken things in a while. Let's rework them into something other people can benefit from.
Error:
gleam run -m lustre/try --target javascript
Compiled in 0.01s
Running lustre/try.main
node:_http_outgoing:879
throw new ERR_INVALID_ARG_TYPE(
^
TypeError [ERR_INVALID_ARG_TYPE]: The "chunk" argument must be of type string or an instance of Buffer or Uint8Array. Received an instance of Error
at write_ (node:_http_outgoing:879:11)
at ServerResponse.end (node:_http_outgoing:1030:5)
at file:///home/wmoore/Source/github.com/lustre-labs/ui/demo/build/dev/javascript/lustre/http.ffi.mjs:82:15 {
code: 'ERR_INVALID_ARG_TYPE'
}
Node.js v20.10.0
Steps to reproduce:
cd ui/demo
gleam run -m lustre/try --target javascript
http://localhost:1234/
Environment:
OS: Arch Linux x86_64
Gleam: gleam 0.33.0
Lustre UI: 680b9ed
Hi, I just tried to follow https://hexdocs.pm/lustre/4.0.0-rc1/guide/01-quickstart.html and I did not look in the URL. So I followed the instructions and ended up with a Version 3 install. Took me as a gleam/lustre beginner some time to figure out what is wrong.
Then I took a look at some examples and in there I found:
Note: this guide is written for Lustre v4. The latest stable release of Lustre is v3. To follow along with this guide, you need to manually edit your gleam.toml and change the required version of lustre to "4.0.0-rc.2".
So I'd suggest to add some info in the quickstart to check for Version 4 to make it easier to follow
Folks using Lustre are unlikely to want to invest in learning JavaScript tooling just to build their apps, and while Gleam compiles to JavaScript just fine, its actual output is not bundled, tree-shaken, or minified, which makes it quite poor to serve on the frontend.
We have an add
subcommand that can download a platform-appropriate esbuild
binary (see the cli branch), it'd be great if we now put that to use and helped folks build their Lustre projects.
$ gleam run -m lustre build app [ entry_module ]
If the entry module is not included, we should read the user's gleam.toml
and assume the entry module is the name of the project.
As a technical detail, we will need to generate a tiny preliminary script that imports the module's main
function and calls it. This will be our entry file to esbuild. That way folks will get a bundle that mounts and starts their app when including on the page.
$ gleam run -m lustre build component [ component_module, ... ]
Similar to building applications, we will need to generate a small script that imports the module's register
function. In the case of bundling multiple components, we will need to import each register
function separately (and make sure we rename them so they are distinct). It's important that we do not use JavaScript's *
import syntax because we want esbuild to properly tree-shake the bundle.
We should treat multiple arguments passed into this command each as a component to include in the same bundle. This way they share a reference to Lustre's runtime and we cut down on bundle size.
We should teat a single module with a trailing *
as a wildcard that include import every module in that path as a component. This way someone could write gleam run -m lustre build component my_app/components/*
and get a bundle of all their components.
os:cmd
.gleam run -m lustre add esbuild
we should fetch it for them instead of erroring.priv/
. For building applications we could call the bundle the name used in gleam.toml
. For individual components we could use the convention {app_name}-{component_name}
. For multiple components we could name the file {app_name}-components
. This could all be configurable via flags.--minify
flag to minify the bundle.Repro case: https://codesandbox.io/p/sandbox/strange-heisenberg-s449ny?file=%2Fsrc%2Fapp.gleam%3A46%2C16
First, comp-b should be rendered (as it is). When the counter is raised above 5, comp-a should be rendered instead, but it never is. If you set a breakpoint in createElement
, you can see that comp-a is created, but it never appears in the DOM.
Re: https://discord.com/channels/768594524158427167/768594524158427170/1163057856514375761
In
Line 195 in dbf232a
prev
is being compared to decoded[0]
but prev
is a
and decoded[0]
is SomeMessage(a)
, so the equality check will always fail and cause a re-render.Hi and first thanks for the package!
I tried to setup a Lustre application by using Vite to leverage on existing tooling (live reload, etc.).
Unfortunately, Vite refuses to build when internal node modules (like child_process
) are required. Because of the imports chain, the CLI is imported in the Lustre package, and Vite refuses to build because of the presence of node modules.
Because of the nature of Lustre to be 100% browser compatible, I think it could gains to separate the CLI from the package, to avoid requiring node modules in browser.
Right now, to make it work, I had to do something like this:
// vite.config.js
import * as path from 'path'
import gleam from 'vite-gleam'
const childProcess = path.resolve(process.cwd(), './src/polyfills/child_process.js')
const fs = path.resolve(process.cwd(), './src/polyfills/fs.js')
export default {
resolve: {
alias: [
{ find: 'node:child_process', replacement: childProcess },
{ find: 'node:fs', replacement: fs },
],
},
plugins: [gleam()],
}
// src/polyfills/child_process.js
export const spawnSync = () => {}
// src/polyfills/fs.js
export const statSync = () => {}
export default {}
It's working, and the main function will never be used in the frontend, so it's not a big deal, but it would greatly improve as a quality-of-life to kickstart a project.
It could easily be separated by having a lustre-cli
package, no? Unless it's not compatible with gleam -m lustre
? In this case, isn't it possible to not bundle the CLI parts in browser?
Tailwind is a popular library for quick styling. Many other frameworks and tools include official support for Tailwind like Phoenix or Rails, so devs coming to Lustre might expect the same.
We should expand lustre add
to include support for the standalone Tailwind binary. Additionally we should detect if a project has a tailwind.config.js
and if it does, download and run Tailwind automatically on build.
lustre add tailwind
:
tailwind.config.js
in the project root:/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.{gleam,mjs}"],
theme: {
extend: {},
},
plugins: [],
}
lustre build
:
tailwind.config.js
is present in the project root, we should attempt to download tailwind if it is not already in build/.lustre/bin
Using boolean attributes on elements doesn't appear to work as expected, I'm only using lustre to render html elements to strings, so I'm not sure if this affects the SPA side of things.
For example:
import gleam/io
import lustre/attribute
import lustre/element
import lustre/element/html
// executing the following I would expect "<input disabled>", but we get "<input>"
pub fn main() {
html.input([attribute.disabled(True)])
|> element.to_string
|> io.println
}
I would assume that this is because boolean attributes are being registered as properties, instead of attributes, and so we are never hitting the boolean case here, but that's just a guess from a quick glance at the code.
Happy to take a look and make a PR
While putting together a collaborative whiteboard app, I noticed the client runtime was received updated patches for newly created nodes. This was causing runtime errors in the vdom code because updates are expected to have a previous node to diff against and there wasn't one!
element.advanced
passes arguments to Element
so that tag and namespace are swapped:
lustre/src/lustre/element.gleam
Line 157 in d3cc411
Setting the alt
attribute to an empty string adds the attribute and removes it instantly. This causes constant modifications to the element (on every update), and the inability to actually set alt=""
(which I think is a valid way to say "this image is not relevant to screen readers"). As a workaround, aria-hidden="true"
can be used for images.
Constant churn in DOM element:
https://github.com/lustre-labs/lustre/assets/273137/e65f684d-5203-44dc-b73d-7715401f0b62
Nobody has asked me to do this, directly; but @hayleigh-dot-dev has hinted at wanting something like this on more than one occasion and also has told me to “work on whatever’s fun”. So I’m going to take a stab at this, and we’ll see how it goes.
Right now void HTML elements like <br>
and <img>
are generated with closing tags when rendering to string or string builder. This is incorrect.
I'm about to push an ad-hoc fix that checks the tag against the list of known HTML void elements (it's finite) and renders them without the closing tag, but this doesn't scale because certain SVG and MathML elements are required to be self-closing like <circle />
. We'll want a better way to deal with this than maintaining a list of hardcoded tags.
Running a lustre project these days spits out this error in the console:
Warning: ReactDOM.render is no longer supported in React 18.
Use createRoot instead. Until you switch to the new API, your
app will behave as if it's running React 17. Learn more:
https://reactjs.org/link/switch-to-createroot
It doesn't stop the app working but it'd be good to migrate over to the new way of doing things anyway.
When setting selected
on an option
in a select
at first paint, the field is not the one selected.
Demo
Gleam code as pseudo-code
h.select([event.on_input(on_cs_input), s.select_cs()], {
use item <- list.map(list_)
let selected = selected_item == item
h.option([a.value(as_s), a.selected(selected)], as_s)
})
HTML produced
<select class="css-0006">
<option value="Ayu Dark" selected="false">Ayu Dark</option>
<option value="Ayu Light" selected="true">Ayu Light</option>
<option value="Gleam" selected="false">Gleam</option>
</select>
And yet, it displays "Gleam" (the last one).
Lustre's Error
type has a few error variants that would be much more helpful for folks if they provided some additional context:
BadComponentName
should include the bad nameComponentAlreadyRegistered
should include the component nameElementNotFound
should include the query selectorWe should wait for the Lustre Server Component branch to be merged in to main before we work on this. This will need to touch some of the FFI code in order to work properly (these errors are constructed in JavaScript)
If we want people to take lustre seriously, we should probably have some way of testing the library to make sure things are working.
Probably the way forward for the runtime things is to use jsdom but we'd have to investigate their Custom Elements support if we want to have tests for lustre's components.
For other tests we could use gleeunit directly, for example when testing the output of element.to_string
.
In the readme file, there's a link to https://hexdocs.pm/docs which leads to a Page Not Found page.
As a convenience it is possible to supply more than one class
attribute and have the runtime concatenate them.
html.aside(
[
attribute.style([#("align-self", "start")]),
attribute.class("relative sticky top-0 hidden px-4 pb-10 h-screen"),
attribute.class("lg:block lg:col-span-2"),
attribute.class("xl:col-span-2"),
For things like tailwind in particular this is a quite nice QOL feature, but when statically rendering a lustre element to a string or string builder these classes render individually such that the above element is emitted as
<aside
style="align-self: start"
class="relative sticky top-0 hidden px-4 pb-10 h-screen"
class="lg:block lg:col-span-2"
class="xl:col-span-2"
>
This ultimately means only the first class attribute is considered by the browser.
To label option elements, we put text inside the option
element like so:
<select>
<option value="1">Option 1</option>
<option value="2">Option 2</option>
<option value="3">Option 3</option>
</select>
But currently, html.option
doesn't take in children as an argument. It only takes in attributes. So the equivalent gleam doesn't work:
html.select([], [
html.option([attribute.value("1")], [element.text("Option 1")]),
html.option([attribute.value("2")], [element.text("Option 2")]),
html.option([attribute.value("3")], [element.text("Option 3")]),
])
first reported in #77
Because lustre/try
is just a simple server for quick experimentation it makes sense for the server to send headers to disable clients caching responses. In the worst-case this doesn't really do anything but in some cases it will help prevent a lot of confusion/frustrating if old content gets served!
The CLI has commands for building applications or starting up a development server. Currently these only look at the project's main Gleam file but there are scenarios where a user might want custom startup logic in an index.js
or want to customise the HTML shell with a custom index.html
.
Our tooling should automatically respect these files if they exist.
tabindex attribute is persisting on elements where it is no longer set.
In the attached screenshot the blue highlighed line has a "tabindex" value that is not set in the render function.
I am 100% sure that it's not set in the render function because the circled div is created in the same list.map
call.
There is a tabindex value set in similarly nested div in a different component. which is the one highlighted below. So I think for some reason it is not being cleared when diffing properly
The code is here https://github.com/CrowdHailer/eyg-lang/blob/4ae1953e77e3b0fdab13c83eaf8ae997a2b59bd2/eyg/src/spotless/view/page.gleam#L21-L38
page.surface
is the function that is used to render the very last element and sets a tabindex.
render.top
is the function that renders an element without tabindex set.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.