Fork of daniellemaywood.uk/gleam — Wasm codegen work
6.4 kB
250 lines
1use std::collections::{HashMap, HashSet};
2
3use ecow::EcoString;
4use itertools::Itertools;
5
6use crate::{
7 docvec,
8 javascript::{INDENT, JavaScriptCodegenTarget},
9 pretty::{Document, Documentable, break_, concat, join, line},
10};
11
12/// A collection of JavaScript import statements from Gleam imports and from
13/// external functions, to be rendered into a JavaScript module.
14///
15#[derive(Debug, Default)]
16pub(crate) struct Imports<'a> {
17 imports: HashMap<EcoString, Import<'a>>,
18 exports: HashSet<EcoString>,
19}
20
21impl<'a> Imports<'a> {
22 pub fn new() -> Self {
23 Self::default()
24 }
25
26 pub fn register_export(&mut self, export: EcoString) {
27 let _ = self.exports.insert(export);
28 }
29
30 pub fn register_module(
31 &mut self,
32 path: EcoString,
33 aliases: impl IntoIterator<Item = EcoString>,
34 unqualified_imports: impl IntoIterator<Item = Member<'a>>,
35 ) {
36 let import = self
37 .imports
38 .entry(path.clone())
39 .or_insert_with(|| Import::new(path.clone()));
40 import.aliases.extend(aliases);
41 import.unqualified.extend(unqualified_imports)
42 }
43
44 pub fn into_doc(self, codegen_target: JavaScriptCodegenTarget) -> Document<'a> {
45 let imports = concat(
46 self.imports
47 .into_values()
48 .sorted_by(|a, b| a.path.cmp(&b.path))
49 .map(|import| Import::into_doc(import, codegen_target)),
50 );
51
52 if self.exports.is_empty() {
53 imports
54 } else {
55 let names = join(
56 self.exports
57 .into_iter()
58 .sorted()
59 .map(|string| string.to_doc()),
60 break_(",", ", "),
61 );
62 let names = docvec![
63 docvec![break_("", " "), names].nest(INDENT),
64 break_(",", " ")
65 ]
66 .group();
67
68 let export_keyword = match codegen_target {
69 JavaScriptCodegenTarget::JavaScript => "export {",
70 JavaScriptCodegenTarget::TypeScriptDeclarations => "export type {",
71 };
72
73 imports
74 .append(line())
75 .append(export_keyword)
76 .append(names)
77 .append("};")
78 .append(line())
79 }
80 }
81
82 pub fn is_empty(&self) -> bool {
83 self.imports.is_empty() && self.exports.is_empty()
84 }
85}
86
87#[derive(Debug)]
88struct Import<'a> {
89 path: EcoString,
90 aliases: HashSet<EcoString>,
91 unqualified: Vec<Member<'a>>,
92}
93
94impl<'a> Import<'a> {
95 fn new(path: EcoString) -> Self {
96 Self {
97 path,
98 aliases: Default::default(),
99 unqualified: Default::default(),
100 }
101 }
102
103 pub fn into_doc(self, codegen_target: JavaScriptCodegenTarget) -> Document<'a> {
104 let path = self.path.to_doc();
105 let import_modifier = if codegen_target == JavaScriptCodegenTarget::TypeScriptDeclarations {
106 "type "
107 } else {
108 ""
109 };
110 let alias_imports = concat(self.aliases.into_iter().sorted().map(|alias| {
111 docvec![
112 "import ",
113 import_modifier,
114 "* as ",
115 alias,
116 " from \"",
117 path.clone(),
118 r#"";"#,
119 line()
120 ]
121 }));
122 if self.unqualified.is_empty() {
123 alias_imports
124 } else {
125 let members = self.unqualified.into_iter().map(Member::into_doc);
126 let members = join(members, break_(",", ", "));
127 let members = docvec![
128 docvec![break_("", " "), members].nest(INDENT),
129 break_(",", " ")
130 ]
131 .group();
132 docvec![
133 alias_imports,
134 "import ",
135 import_modifier,
136 "{",
137 members,
138 "} from \"",
139 path,
140 r#"";"#,
141 line()
142 ]
143 }
144 }
145}
146
147#[derive(Debug)]
148pub struct Member<'a> {
149 pub name: Document<'a>,
150 pub alias: Option<Document<'a>>,
151}
152
153impl<'a> Member<'a> {
154 fn into_doc(self) -> Document<'a> {
155 match self.alias {
156 None => self.name,
157 Some(alias) => docvec![self.name, " as ", alias],
158 }
159 }
160}
161
162#[test]
163fn into_doc() {
164 let mut imports = Imports::new();
165 imports.register_module("./gleam/empty".into(), [], []);
166 imports.register_module(
167 "./multiple/times".into(),
168 ["wibble".into(), "wobble".into()],
169 [],
170 );
171 imports.register_module("./multiple/times".into(), ["wubble".into()], []);
172 imports.register_module(
173 "./multiple/times".into(),
174 [],
175 [Member {
176 name: "one".to_doc(),
177 alias: None,
178 }],
179 );
180
181 imports.register_module(
182 "./other".into(),
183 [],
184 [
185 Member {
186 name: "one".to_doc(),
187 alias: None,
188 },
189 Member {
190 name: "one".to_doc(),
191 alias: Some("onee".to_doc()),
192 },
193 Member {
194 name: "two".to_doc(),
195 alias: Some("twoo".to_doc()),
196 },
197 ],
198 );
199
200 imports.register_module(
201 "./other".into(),
202 [],
203 [
204 Member {
205 name: "three".to_doc(),
206 alias: None,
207 },
208 Member {
209 name: "four".to_doc(),
210 alias: None,
211 },
212 ],
213 );
214
215 imports.register_module(
216 "./zzz".into(),
217 [],
218 [
219 Member {
220 name: "one".to_doc(),
221 alias: None,
222 },
223 Member {
224 name: "two".to_doc(),
225 alias: None,
226 },
227 ],
228 );
229
230 assert_eq!(
231 line()
232 .append(imports.into_doc(JavaScriptCodegenTarget::JavaScript))
233 .to_pretty_string(40),
234 r#"
235import * as wibble from "./multiple/times";
236import * as wobble from "./multiple/times";
237import * as wubble from "./multiple/times";
238import { one } from "./multiple/times";
239import {
240 one,
241 one as onee,
242 two as twoo,
243 three,
244 four,
245} from "./other";
246import { one, two } from "./zzz";
247"#
248 .to_string()
249 );
250}