Lustre's CLI and development tooling: zero-config dev server, bundling, and scaffolding.
at main 6.6 kB view raw
1//// The `cli` module is how we create "scripts" that are intended to be run from 2//// the command line. 3 4// IMPORTS --------------------------------------------------------------------- 5 6import gleam/dict.{type Dict} 7import gleam/erlang 8import gleam/io 9import gleam/list 10import gleam/result 11import gleam_community/ansi 12import glint 13import lustre_dev_tools/error.{type Error, TemplateMissing} 14import lustre_dev_tools/project.{type Config} 15import simplifile 16import spinner.{type Spinner} 17import tom 18 19// TYPES ----------------------------------------------------------------------- 20 21/// 22/// 23pub opaque type Cli(a) { 24 Cli(run: fn(Env) -> #(Env, Result(a, Error))) 25} 26 27type Env { 28 Env(muted: Bool, spinner: SpinnerStatus, flags: glint.Flags, config: Config) 29} 30 31type SpinnerStatus { 32 Running(spinner: Spinner, message: String) 33 Paused 34} 35 36// RUNNING CLI SCRIPTS --------------------------------------------------------- 37 38/// 39/// 40pub fn run(step: Cli(a), flags: glint.Flags) -> Result(a, Error) { 41 use config <- result.try(project.config()) 42 let env = Env(muted: False, spinner: Paused, flags: flags, config: config) 43 let #(env, result) = step.run(env) 44 45 case env.spinner { 46 Running(spinner, _) -> spinner.stop(spinner) 47 Paused -> Nil 48 } 49 50 case result, env.spinner { 51 // In case the spinner was still running when we got an error we print 52 // the message where the spinner got stuck. 53 Error(_), Running(_, message) -> io.println("" <> ansi.red(message)) 54 Error(_), _ | Ok(_), _ -> Nil 55 } 56 57 result 58} 59 60// CREATING CLI SCRIPTS FROM SIMPLE VALUES ------------------------------------- 61 62/// 63/// 64pub fn return(value: a) -> Cli(a) { 65 use env <- Cli 66 67 #(env, Ok(value)) 68} 69 70/// 71/// 72pub fn throw(error: Error) -> Cli(a) { 73 use env <- Cli 74 75 #(env, Error(error)) 76} 77 78pub fn from_result(result: Result(a, Error)) -> Cli(a) { 79 use env <- Cli 80 81 #(env, result) 82} 83 84// COMBINATORS ----------------------------------------------------------------- 85 86/// 87/// 88pub fn do(step: Cli(a), then next: fn(a) -> Cli(b)) -> Cli(b) { 89 use env <- Cli 90 let #(env, result) = step.run(env) 91 92 case result { 93 Ok(value) -> next(value).run(env) 94 Error(error) -> { 95 case env.spinner { 96 Running(spinner, _message) -> spinner.stop(spinner) 97 Paused -> Nil 98 } 99 100 #(env, Error(error)) 101 } 102 } 103} 104 105pub fn in(value: fn() -> a) -> Cli(a) { 106 use env <- Cli 107 108 #(env, Ok(value())) 109} 110 111pub fn map(step: Cli(a), then next: fn(a) -> b) -> Cli(b) { 112 use env <- Cli 113 let #(env, result) = step.run(env) 114 let result = result.map(result, next) 115 116 #(env, result) 117} 118 119/// 120/// 121pub fn try(result: Result(a, Error), then next: fn(a) -> Cli(b)) -> Cli(b) { 122 use env <- Cli 123 124 case result { 125 Ok(a) -> next(a).run(env) 126 Error(error) -> { 127 case env.spinner { 128 Running(spinner, _message) -> spinner.stop(spinner) 129 Paused -> Nil 130 } 131 132 #(env, Error(error)) 133 } 134 } 135} 136 137// LOGGING --------------------------------------------------------------------- 138 139/// 140/// 141pub fn log(message: String, then next: fn() -> Cli(a)) -> Cli(a) { 142 use env <- Cli 143 let env = case env.muted { 144 True -> env 145 False -> 146 Env( 147 ..env, 148 spinner: case env.spinner { 149 Paused -> 150 Running( 151 spinner.new(message) 152 |> spinner.with_colour(ansi.magenta) 153 |> spinner.with_frames(spinner.snake_frames) 154 |> spinner.start, 155 message, 156 ) 157 158 Running(spinner, _) -> { 159 spinner.set_text(spinner, message) 160 Running(spinner, message) 161 } 162 }, 163 ) 164 } 165 166 next().run(env) 167} 168 169pub fn success(message: String, then next: fn() -> Cli(a)) -> Cli(a) { 170 use env <- Cli 171 let env = 172 Env( 173 ..env, 174 spinner: case env.spinner { 175 Paused -> Paused 176 Running(spinner, _) -> { 177 spinner.stop(spinner) 178 Paused 179 } 180 }, 181 ) 182 183 case env.muted { 184 True -> Nil 185 False -> io.println("" <> ansi.green(message)) 186 } 187 188 next().run(env) 189} 190 191pub fn notify(message: String, then next: fn() -> Cli(a)) -> Cli(a) { 192 use env <- Cli 193 let env = 194 Env( 195 ..env, 196 spinner: case env.spinner { 197 Paused -> Paused 198 Running(spinner, _) -> { 199 spinner.stop(spinner) 200 Paused 201 } 202 }, 203 ) 204 205 case env.muted { 206 True -> Nil 207 False -> io.println(ansi.bright_cyan(message)) 208 } 209 210 next().run(env) 211} 212 213pub fn mute() -> Cli(Nil) { 214 use env <- Cli 215 216 #(Env(..env, muted: True), Ok(Nil)) 217} 218 219pub fn unmute() -> Cli(Nil) { 220 use env <- Cli 221 222 #(Env(..env, muted: False), Ok(Nil)) 223} 224 225// UTILS ----------------------------------------------------------------------- 226 227pub fn template(name: String, then next: fn(String) -> Cli(a)) -> Cli(a) { 228 use env <- Cli 229 let assert Ok(priv) = erlang.priv_directory("lustre_dev_tools") 230 231 case simplifile.read(priv <> "/template/" <> name) { 232 Ok(template) -> next(template).run(env) 233 Error(error) -> #(env, Error(TemplateMissing(name, error))) 234 } 235} 236 237// ENV ------------------------------------------------------------------------- 238 239pub fn get_config() -> Cli(Config) { 240 use env <- Cli 241 242 #(env, Ok(env.config)) 243} 244 245pub fn get_name() -> Cli(String) { 246 use env <- Cli 247 248 #(env, Ok(env.config.name)) 249} 250 251// FLAGS ----------------------------------------------------------------------- 252 253pub fn get_flags() -> Cli(glint.Flags) { 254 use env <- Cli 255 256 #(env, Ok(env.flags)) 257} 258 259pub fn get_config_value( 260 name: String, 261 fallback: a, 262 namespace: List(String), 263 toml: fn(Dict(String, tom.Toml), List(String)) -> Result(a, _), 264 flag: fn(glint.Flags) -> Result(a, _), 265) -> Cli(a) { 266 use env <- Cli 267 let toml_path = list.concat([["lustre-dev"], namespace, [name]]) 268 let value = 269 result.or( 270 result.nil_error(flag(env.flags)), 271 result.nil_error(toml(env.config.toml, toml_path)), 272 ) 273 |> result.unwrap(fallback) 274 275 #(env, Ok(value)) 276} 277 278pub fn get_int( 279 name: String, 280 fallback: Int, 281 namespace: List(String), 282 flag: fn(glint.Flags) -> Result(Int, _), 283) -> Cli(Int) { 284 get_config_value(name, fallback, namespace, tom.get_int, flag) 285} 286 287pub fn get_string( 288 name: String, 289 fallback: String, 290 namespace: List(String), 291 flag: fn(glint.Flags) -> Result(String, _), 292) -> Cli(String) { 293 get_config_value(name, fallback, namespace, tom.get_string, flag) 294} 295 296pub fn get_bool( 297 name: String, 298 fallback: Bool, 299 namespace: List(String), 300 flag: fn(glint.Flags) -> Result(Bool, _), 301) -> Cli(Bool) { 302 get_config_value(name, fallback, namespace, tom.get_bool, flag) 303} 304// CONFIG FETCHING -----------------------------------------------------------------------