Lustre's CLI and development tooling: zero-config dev server, bundling, and scaffolding.
at main 17 kB view raw
1// IMPORTS --------------------------------------------------------------------- 2 3import gleam/bit_array 4import gleam/dynamic.{type Dynamic} 5import gleam/int 6import gleam/io 7import gleam/list 8import gleam/otp/actor 9import gleam/package_interface.{type Type, Fn, Named, Tuple, Variable} 10import gleam/string 11import glisten 12import simplifile 13 14// TYPES ----------------------------------------------------------------------- 15 16pub type Error { 17 BuildError(reason: String) 18 BundleError(reason: String) 19 CannotCreateDirectory(reason: simplifile.FileError, path: String) 20 CannotReadFile(reason: simplifile.FileError, path: String) 21 CannotSetPermissions(reason: simplifile.FileError, path: String) 22 CannotStartDevServer(reason: glisten.StartError) 23 CannotStartFileWatcher(reason: actor.StartError) 24 CannotWriteFile(reason: simplifile.FileError, path: String) 25 ComponentMissing(module: String) 26 IncompleteProxy(missing: List(String)) 27 InternalError(message: String) 28 InvalidProxyTarget(to: String) 29 MainBadAppType(module: String, flags: Type, model: Type, msg: Type) 30 MainMissing(module: String) 31 MainTakesAnArgument(module: String, got: Type) 32 ModuleMissing(module: String) 33 NameIncorrectType(module: String, got: Type) 34 NameMissing(module: String) 35 NetworkError(Dynamic) 36 TemplateMissing(name: String, reason: simplifile.FileError) 37 UnknownPlatform(binary: String, os: String, cpu: String) 38 OtpTooOld(version: Int) 39 UnzipError(Dynamic) 40 InvalidEsbuildBinary 41 InvalidTailwindBinary 42} 43 44// CONVERSIONS ----------------------------------------------------------------- 45 46pub fn explain(error: Error) -> Nil { 47 case error { 48 BuildError(reason) -> build_error(reason) 49 BundleError(reason) -> bundle_error(reason) 50 CannotCreateDirectory(reason, path) -> cannot_create_directory(reason, path) 51 CannotReadFile(reason, path) -> cannot_read_file(reason, path) 52 CannotSetPermissions(reason, path) -> cannot_set_permissions(reason, path) 53 CannotStartDevServer(reason) -> cannot_start_dev_server(reason) 54 CannotStartFileWatcher(reason) -> cannot_start_file_watcher(reason) 55 CannotWriteFile(reason, path) -> cannot_write_file(reason, path) 56 ComponentMissing(module) -> component_missing(module) 57 IncompleteProxy(missing) -> incomplete_proxy(missing) 58 InternalError(message) -> internal_error(message) 59 InvalidProxyTarget(to) -> invalid_proxy_target(to) 60 MainBadAppType(module, flags, model, msg) -> 61 main_bad_app_type(module, flags, model, msg) 62 MainMissing(module) -> main_missing(module) 63 MainTakesAnArgument(module, got) -> main_takes_an_argument(module, got) 64 ModuleMissing(module) -> module_missing(module) 65 NameIncorrectType(module, got) -> name_incorrect_type(module, got) 66 NameMissing(module) -> name_missing(module) 67 NetworkError(error) -> network_error(error) 68 TemplateMissing(name, reason) -> template_missing(name, reason) 69 UnknownPlatform(binary, os, cpu) -> unknown_platform(binary, os, cpu) 70 OtpTooOld(version) -> otp_too_old(version) 71 UnzipError(error) -> unzip_error(error) 72 InvalidEsbuildBinary -> invalid_esbuild_binary() 73 InvalidTailwindBinary -> invalid_tailwind_binary() 74 } 75 |> io.print_error 76} 77 78fn build_error(reason: String) -> String { 79 let message = 80 " 81It looks like your project has some compilation errors that need to be addressed 82before I can do anything. Here's the error message I got: 83 84{reason} 85" 86 87 message 88 |> string.replace("{reason}", reason) 89} 90 91fn bundle_error(reason: String) -> String { 92 let message = 93 " 94I ran into an unexpected issue while trying to bundle your project with esbuild. 95Here's the error message I got: 96 97 {reason} 98 99If you think this is a bug, please open an issue at 100https://github.com/lustre-labs/dev-tools/issues/new with some details about what 101you were trying to do when you ran into this issue. 102" 103 104 message 105 |> string.replace("{reason}", reason) 106} 107 108fn cannot_create_directory(reason: simplifile.FileError, path: String) -> String { 109 let message = 110 " 111I ran into an error while trying to create the following directory: 112 113 {path} 114 115Here's the error message I got: 116 117 {reason} 118 119If you think this is a bug, please open an issue at 120https://github.com/lustre-labs/dev-tools/issues/new with some details about what 121you were trying to do when you ran into this issue. 122" 123 124 message 125 |> string.replace("{path}", path) 126 |> string.replace("{reason}", string.inspect(reason)) 127} 128 129fn cannot_read_file(reason: simplifile.FileError, path: String) -> String { 130 let message = 131 " 132I ran into an error while trying to read the following file: 133 134 {path} 135 136Here's the error message I got: 137 138 {reason} 139 140If you think this is a bug, please open an issue at 141https://github.com/lustre-labs/dev-tools/issues/new with some details about what 142you were trying to do when you ran into this issue. 143" 144 145 message 146 |> string.replace("{path}", path) 147 |> string.replace("{reason}", string.inspect(reason)) 148} 149 150fn cannot_set_permissions(reason: simplifile.FileError, path: String) -> String { 151 let message = 152 " 153I ran into an error while trying to set the permissions on the following file: 154 155 {path} 156 157Here's the error message I got: 158 159 {reason} 160 161If you think this is a bug, please open an issue at 162https://github.com/lustre-labs/dev-tools/issues/new with some details about what 163you were trying to do when you ran into this issue. 164" 165 166 message 167 |> string.replace("{path}", path) 168 |> string.replace("{reason}", string.inspect(reason)) 169} 170 171fn cannot_start_dev_server(reason: glisten.StartError) -> String { 172 let message = 173 " 174I ran into an error while trying to start the development server. Here's the 175error message I got: 176 177 {reason} 178 179Please open an issue at https://github.com/lustre-labs/dev-tools/issues/new with 180some details about what you were trying to do when you ran into this issue. 181" 182 183 message 184 |> string.replace("{reason}", string.inspect(reason)) 185} 186 187fn cannot_start_file_watcher(reason: actor.StartError) -> String { 188 let message = 189 " 190I ran into an error while trying to start the file watcher used for live reloading. 191Here's the error message I got: 192 193 {reason} 194 195Please open an issue at https://github.com/lustre-labs/dev-tools/issues/new with 196some details about what you were trying to do when you ran into this issue. 197" 198 199 message 200 |> string.replace("{reason}", string.inspect(reason)) 201} 202 203fn cannot_write_file(reason: simplifile.FileError, path: String) -> String { 204 let message = 205 " 206I ran into an error while trying to write the following file: 207 208 {path} 209 210Here's the error message I got: 211 212 {reason} 213 214If you think this is a bug, please open an issue at 215https://github.com/lustre-labs/dev-tools/issues/new with some details about what 216you were trying to do when you ran into this issue. 217" 218 219 message 220 |> string.replace("{path}", path) 221 |> string.replace("{reason}", string.inspect(reason)) 222} 223 224fn component_missing(module: String) -> String { 225 let message = 226 " 227I couldn't find a valid component definition in the following module: 228 229 {module} 230 231To bundle a component, the module should have a public function that returns a 232Lustre `App`. Try adding a function like this: 233 234 pub const name: String = \"my-component\" 235 236 pub fn component() -> App(Nil, Model, Msg) { 237 lustre.component(init, update, view, on_attribute_change()) 238 } 239" 240 241 message 242 |> string.replace("{module}", module) 243} 244 245fn incomplete_proxy(missing: List(String)) -> String { 246 let message = 247 " 248I'm missing some information needed to proxy requests from the development server. 249The following keys are missing: 250 251 {missing} 252 253You can provide the missing information either as flags when starting the 254development server, or by adding a `proxy` key to the `lustre-dev` section of 255your `gleam.toml`. 256 257To pass the information as flags, you should start the development server like 258this: 259 260 gleam run -m lustre/dev start --proxy-from=/api --proxy-to=http://localhost:4000/api 261 262To add the information to your `gleam.toml`, make sure it looks something like 263this: 264 265 [lustre-dev.start] 266 proxy = { from = \"/api\", to = \"http://localhost:4000/api\" } 267" 268 269 message 270 |> string.replace("{missing}", string.join(missing, ", ")) 271} 272 273fn internal_error(info: String) -> String { 274 let message = 275 " 276Oops, it looks like I ran into an unexpected error while trying to do something. 277Please open an issue at https://github.com/lustre-labs/dev-tools/issues/new with 278the following message: 279 280 {info} 281" 282 283 message 284 |> string.replace("{info}", info) 285} 286 287pub fn invalid_proxy_target(to: String) -> String { 288 let message = 289 " 290I ran into an issue reading your proxy configuration. The URI you provided as the 291target for the proxy is invalid: 292 293 {to} 294 295Please make sure the URI is valid and try again. If you think this is a bug, 296please open an issue at https://github.com/lustre-labs/dev-tools/issues/new 297" 298 299 message 300 |> string.replace("{to}", to) 301} 302 303fn main_bad_app_type( 304 module: String, 305 flags: Type, 306 model: Type, 307 msg: Type, 308) -> String { 309 let message = 310 " 311I don't know how to serve the Lustre app returned from the `main` function in the 312following module: 313 314 {module} 315 316I worked out your app type to be: 317 318 App({flags}, {model}, {msg}) 319 320I need your app's flags type to either be `Nil` or a type variable like `a`. Your 321`main` function should look something like this: 322 323 pub fn main() -> App(Nil, {model}, {msg}) { 324 lustre.application(init, update, view) 325 } 326 327I don't know how to produce flags of type `{flags}`! If this is intentional and 328you want to provide your own flags, try modifying your `main` function to look 329like this: 330 331 pub fn main() -> Nil { 332 let app = lustre.application(init, update, view) 333 let flags = todo // provide your flags here 334 let assert Ok() = lustre.run(app, \"#app\", flags) 335 336 Nil 337 } 338" 339 340 message 341 |> string.replace("{module}", module) 342 |> string.replace("{flags}", pretty_type(flags)) 343 |> string.replace("{model}", pretty_type(model)) 344 |> string.replace("{msg}", pretty_type(msg)) 345} 346 347fn main_missing(module: String) -> String { 348 let message = 349 " 350I couldn't find a `main` function in the following module: 351 352 {module} 353 354Is the module path correct? Your app's `main` function is the entry point we use 355to build and start your app. It should look something like this: 356 357 pub fn main() -> App(Nil, Model, Msg) { 358 lustre.application(init, update, view) 359 } 360" 361 362 message 363 |> string.replace("{module}", module) 364} 365 366fn main_takes_an_argument(module: String, got: Type) -> String { 367 let message = 368 " 369I ran into a problem trying to serve your Lustre app in the following module: 370 371 {module} 372 373I worked out the type of your `main` function to be: 374 375 {got} 376 377The `main` function should not take any arguments because I don't know how to 378provide them! Your `main` function should look something like this: 379 380 pub fn main() -> App(Nil, Model, Msg) { 381 lustre.application(init, update, view) 382 } 383" 384 385 message 386 |> string.replace("{module}", module) 387 |> string.replace("{got}", pretty_type(got)) 388} 389 390fn module_missing(module: String) -> String { 391 let message = 392 " 393I couldn't find the following module: 394 395 {module} 396 397Make sure the module path is correct and also the module is not included in the 398`internal_modules` list in your `gleam.toml`. 399 400If you think this is a bug, please open an issue at 401https://github.com/lustre-labs/dev-tools/issues/new with some details about what 402you were trying to do when you ran into this issue. 403" 404 405 message 406 |> string.replace("{module}", module) 407} 408 409fn name_incorrect_type(module: String, got: Type) -> String { 410 let message = 411 " 412I ran into a problem trying to bundle the component in the following module: 413 414 {module} 415 416The type of the `name` constant isn't what I expected. I worked out the type to 417be: 418 419 {got} 420 421The `name` constant should be a string. Make sure it's defined like this: 422 423 pub const name: String = \"my-component\" 424 425If you think this is a bug, please open an issue at 426https://github.com/lustre-labs/dev-tools/issues/new with some details about what 427you were trying to do when you ran into this issue. 428" 429 430 message 431 |> string.replace("{module}", module) 432 |> string.replace("{got}", pretty_type(got)) 433} 434 435fn name_missing(module: String) -> String { 436 let message = 437 " 438I couldn't find a valid component definition in the following module: 439 440 {module} 441 442To bundle a component, the module should have a public function that returns a 443Lustre `App`. Try adding a function like this: 444 445 pub const name: String = \"my-component\" 446 447 pub fn component() -> App(Nil, Model, Msg) { 448 lustre.component(init, update, view, on_attribute_change()) 449 } 450" 451 452 message 453 |> string.replace("{module}", module) 454} 455 456fn network_error(error: Dynamic) -> String { 457 let message = 458 " 459I ran into an unexpected network error while trying to do something. Here's the 460error message I got: 461 462 {error} 463 464Please check your internet connection and try again. If you think this is a bug, 465please open an issue at https://github.com/lustre-labs/dev-tools/issues/new with 466some details about what you were trying to do when you ran into this issue. 467" 468 469 message 470 |> string.replace("{error}", string.inspect(error)) 471} 472 473fn template_missing(name: String, reason: simplifile.FileError) -> String { 474 let message = 475 " 476I ran into an unexpected error trying to read an internal template file. This 477should never happen! The template file I was looking for is: 478 479 {name} 480 481The error message I got was: 482 483 {reason} 484 485Please open an issue at https://github.com/lustre-labs/dev-tools/issues/new with 486the above information and some details about what you were trying to do when you 487ran into this issue. 488} 489" 490 491 message 492 |> string.replace("{name}", name) 493 |> string.replace("{reason}", string.inspect(reason)) 494} 495 496fn unknown_platform(binary: String, os: String, cpu: String) -> String { 497 let path = "./build/.lustre/bin/" <> binary 498 let message = 499 " 500I ran into a problem trying to download the {binary} binary. I couldn't find a 501compatible binary for the following platform: 502 503 OS: {os} 504 CPU: {cpu} 505 506You may be able to build the binary from source and place it at the following 507path: 508 509 {path} 510 511If you think this is a bug, please open an issue at 512https://github.com/lustre-labs/dev-tools/issues/new with some details about what 513you were trying to do when you ran into this issue. 514" 515 516 message 517 |> string.replace("{binary}", binary) 518 |> string.replace("{os}", os) 519 |> string.replace("{cpu}", cpu) 520 |> string.replace("{path}", path) 521} 522 523fn otp_too_old(version: Int) -> String { 524 let message = 525 " 526It looks like you're running an OTP version that is not supported by the dev 527tools: {version}. 528 529You should upgrade to OTP 26 or newer to run this command: 530https://gleam.run/getting-started/installing/#installing-erlang 531" 532 533 message 534 |> string.replace("{version}", int.to_string(version)) 535} 536 537fn unzip_error(error: Dynamic) -> String { 538 let message = 539 " 540I ran into an unexpected error while trying to unzip a file. Here's the error 541message I got: 542 543 {error} 544 545If you think this is a bug, please open an issue at 546https://github.com/lustre-labs/dev-tools/issues/new with some details about what 547you were trying to do when you ran into this issue. 548" 549 550 message 551 |> string.replace("{error}", string.inspect(error)) 552} 553 554fn invalid_esbuild_binary() -> String { 555 " 556It looks like the downloaded Esbuild tarball has a different hash from what I 557expected. 558" 559} 560 561fn invalid_tailwind_binary() -> String { 562 " 563It looks like the downloaded Tailwind binary has a different hash from what I 564expected. 565" 566} 567 568// UTILS ----------------------------------------------------------------------- 569 570fn pretty_type(t: Type) -> String { 571 case t { 572 Tuple(elements) -> { 573 let message = "#({elements})" 574 let elements = list.map(elements, pretty_type) 575 576 message 577 |> string.replace("{elements}", string.join(elements, ", ")) 578 } 579 580 Fn(params, return) -> { 581 let message = "fn({params}) -> {return}" 582 let params = list.map(params, pretty_type) 583 let return = pretty_type(return) 584 585 message 586 |> string.replace("{params}", string.join(params, ", ")) 587 |> string.replace("{return}", return) 588 } 589 590 Named(name, _package, _module, []) -> name 591 Named(name, _package, _module, params) -> { 592 let message = "{name}({params})" 593 let params = list.map(params, pretty_type) 594 595 message 596 |> string.replace("{name}", name) 597 |> string.replace("{params}", string.join(params, ", ")) 598 } 599 600 Variable(id) -> pretty_var(id) 601 } 602} 603 604fn pretty_var(id: Int) -> String { 605 case id >= 26 { 606 True -> pretty_var(id / 26 - 1) <> pretty_var(id % 26) 607 608 False -> { 609 let id = id + 97 610 let assert Ok(var) = bit_array.to_string(<<id:int>>) 611 612 var 613 } 614 } 615}