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