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