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}