⭐️ A friendly language for building type-safe, scalable systems!
at main 274 lines 8.4 kB view raw
1use crate::{ 2 Result, 3 analyse::TargetSupport, 4 build::{ 5 ErlangAppCodegenConfiguration, Module, module_erlang_name, package_compiler::StdlibPackage, 6 }, 7 config::PackageConfig, 8 erlang, 9 io::FileSystemWriter, 10 javascript::{self, ModuleConfig}, 11 line_numbers::LineNumbers, 12}; 13use ecow::EcoString; 14use erlang::escape_atom_string; 15use itertools::Itertools; 16use std::fmt::Debug; 17 18use camino::Utf8Path; 19 20/// A code generator that creates a .erl Erlang module and record header files 21/// for each Gleam module in the package. 22#[derive(Debug)] 23pub struct Erlang<'a> { 24 build_directory: &'a Utf8Path, 25 include_directory: &'a Utf8Path, 26} 27 28impl<'a> Erlang<'a> { 29 pub fn new(build_directory: &'a Utf8Path, include_directory: &'a Utf8Path) -> Self { 30 Self { 31 build_directory, 32 include_directory, 33 } 34 } 35 36 pub fn render<Writer: FileSystemWriter>( 37 &self, 38 writer: Writer, 39 modules: &[Module], 40 root: &Utf8Path, 41 ) -> Result<()> { 42 for module in modules { 43 let erl_name = module.erlang_name(); 44 self.erlang_module(&writer, module, &erl_name, root)?; 45 self.erlang_record_headers(&writer, module, &erl_name)?; 46 } 47 Ok(()) 48 } 49 50 fn erlang_module<Writer: FileSystemWriter>( 51 &self, 52 writer: &Writer, 53 module: &Module, 54 erl_name: &str, 55 root: &Utf8Path, 56 ) -> Result<()> { 57 let name = format!("{erl_name}.erl"); 58 let path = self.build_directory.join(&name); 59 let line_numbers = LineNumbers::new(&module.code); 60 let output = erlang::module(&module.ast, &line_numbers, root); 61 tracing::debug!(name = ?name, "Generated Erlang module"); 62 writer.write(&path, &output?) 63 } 64 65 fn erlang_record_headers<Writer: FileSystemWriter>( 66 &self, 67 writer: &Writer, 68 module: &Module, 69 erl_name: &str, 70 ) -> Result<()> { 71 for (name, text) in erlang::records(&module.ast) { 72 let name = format!("{erl_name}_{name}.hrl"); 73 tracing::debug!(name = ?name, "Generated Erlang header"); 74 writer.write(&self.include_directory.join(name), &text)?; 75 } 76 Ok(()) 77 } 78} 79 80/// A code generator that creates a .app Erlang application file for the package 81#[derive(Debug)] 82pub struct ErlangApp<'a> { 83 output_directory: &'a Utf8Path, 84 config: &'a ErlangAppCodegenConfiguration, 85} 86 87impl<'a> ErlangApp<'a> { 88 pub fn new(output_directory: &'a Utf8Path, config: &'a ErlangAppCodegenConfiguration) -> Self { 89 Self { 90 output_directory, 91 config, 92 } 93 } 94 95 pub fn render<Writer: FileSystemWriter>( 96 &self, 97 writer: Writer, 98 config: &PackageConfig, 99 modules: &[Module], 100 native_modules: Vec<EcoString>, 101 ) -> Result<()> { 102 fn tuple(key: &str, value: &str) -> String { 103 format!(" {{{key}, {value}}},\n") 104 } 105 106 let path = self.output_directory.join(format!("{}.app", &config.name)); 107 108 let start_module = config 109 .erlang 110 .application_start_module 111 .as_ref() 112 .map(|module| tuple("mod", &format!("{{'{}', []}}", module_erlang_name(module)))) 113 .unwrap_or_default(); 114 115 let modules = modules 116 .iter() 117 .map(|m| m.erlang_name()) 118 .chain(native_modules) 119 .unique() 120 .sorted() 121 .map(escape_atom_string) 122 .join(",\n "); 123 124 // TODO: When precompiling for production (i.e. as a precompiled hex 125 // package) we will need to exclude the dev deps. 126 let applications = config 127 .dependencies 128 .keys() 129 .chain( 130 config 131 .dev_dependencies 132 .keys() 133 .take_while(|_| self.config.include_dev_deps), 134 ) 135 // TODO: test this! 136 .map(|name| self.config.package_name_overrides.get(name).unwrap_or(name)) 137 .chain(config.erlang.extra_applications.iter()) 138 .sorted() 139 .join(",\n "); 140 141 let text = format!( 142 r#"{{application, {package}, [ 143{start_module} {{vsn, "{version}"}}, 144 {{applications, [{applications}]}}, 145 {{description, "{description}"}}, 146 {{modules, [{modules}]}}, 147 {{registered, []}} 148]}}. 149"#, 150 applications = applications, 151 description = config.description, 152 modules = modules, 153 package = config.name, 154 start_module = start_module, 155 version = config.version, 156 ); 157 158 writer.write(&path, &text) 159 } 160} 161 162#[derive(Debug, Clone, Copy, PartialEq, Eq)] 163pub enum TypeScriptDeclarations { 164 None, 165 Emit, 166} 167 168#[derive(Debug)] 169pub struct JavaScript<'a> { 170 output_directory: &'a Utf8Path, 171 prelude_location: &'a Utf8Path, 172 project_root: &'a Utf8Path, 173 typescript: TypeScriptDeclarations, 174 target_support: TargetSupport, 175} 176 177impl<'a> JavaScript<'a> { 178 pub fn new( 179 output_directory: &'a Utf8Path, 180 typescript: TypeScriptDeclarations, 181 prelude_location: &'a Utf8Path, 182 project_root: &'a Utf8Path, 183 target_support: TargetSupport, 184 ) -> Self { 185 Self { 186 prelude_location, 187 output_directory, 188 target_support, 189 project_root, 190 typescript, 191 } 192 } 193 194 pub fn render( 195 &self, 196 writer: &impl FileSystemWriter, 197 modules: &[Module], 198 stdlib_package: StdlibPackage, 199 ) -> Result<()> { 200 for module in modules { 201 let js_name = module.name.clone(); 202 if self.typescript == TypeScriptDeclarations::Emit { 203 self.ts_declaration(writer, module, &js_name)?; 204 } 205 self.js_module(writer, module, &js_name, stdlib_package)? 206 } 207 self.write_prelude(writer)?; 208 Ok(()) 209 } 210 211 fn write_prelude(&self, writer: &impl FileSystemWriter) -> Result<()> { 212 let rexport = format!("export * from \"{}\";\n", self.prelude_location); 213 let prelude_path = &self.output_directory.join("gleam.mjs"); 214 215 // This check skips unnecessary `gleam.mjs` writes which confuse 216 // watchers and HMR build tools 217 if !writer.exists(prelude_path) { 218 writer.write(prelude_path, &rexport)?; 219 } 220 221 if self.typescript == TypeScriptDeclarations::Emit { 222 let rexport = format!( 223 "export * from \"{}\";\nexport type * from \"{}\";\n", 224 self.prelude_location, 225 self.prelude_location.as_str().replace(".mjs", ".d.mts") 226 ); 227 let prelude_declaration_path = &self.output_directory.join("gleam.d.mts"); 228 229 // Type declaration may trigger badly configured watchers 230 if !writer.exists(prelude_declaration_path) { 231 writer.write(prelude_declaration_path, &rexport)?; 232 } 233 } 234 235 Ok(()) 236 } 237 238 fn ts_declaration( 239 &self, 240 writer: &impl FileSystemWriter, 241 module: &Module, 242 js_name: &str, 243 ) -> Result<()> { 244 let name = format!("{js_name}.d.mts"); 245 let path = self.output_directory.join(name); 246 let output = javascript::ts_declaration(&module.ast, &module.input_path, &module.code); 247 tracing::debug!(name = ?js_name, "Generated TS declaration"); 248 writer.write(&path, &output?) 249 } 250 251 fn js_module( 252 &self, 253 writer: &impl FileSystemWriter, 254 module: &Module, 255 js_name: &str, 256 stdlib_package: StdlibPackage, 257 ) -> Result<()> { 258 let name = format!("{js_name}.mjs"); 259 let path = self.output_directory.join(name); 260 let line_numbers = LineNumbers::new(&module.code); 261 let output = javascript::module(ModuleConfig { 262 module: &module.ast, 263 line_numbers: &line_numbers, 264 path: &module.input_path, 265 project_root: self.project_root, 266 src: &module.code, 267 target_support: self.target_support, 268 typescript: self.typescript, 269 stdlib_package, 270 }); 271 tracing::debug!(name = ?js_name, "Generated js module"); 272 writer.write(&path, &output?) 273 } 274}