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}