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 imports
68 .append(line())
69 .append("export {")
70 .append(names)
71 .append("};")
72 .append(line())
73 }
74 }
75
76 pub fn is_empty(&self) -> bool {
77 self.imports.is_empty() && self.exports.is_empty()
78 }
79}
80
81#[derive(Debug)]
82struct Import<'a> {
83 path: EcoString,
84 aliases: HashSet<EcoString>,
85 unqualified: Vec<Member<'a>>,
86}
87
88impl<'a> Import<'a> {
89 fn new(path: EcoString) -> Self {
90 Self {
91 path,
92 aliases: Default::default(),
93 unqualified: Default::default(),
94 }
95 }
96
97 pub fn into_doc(self, codegen_target: JavaScriptCodegenTarget) -> Document<'a> {
98 let path = self.path.to_doc();
99 let import_modifier = if codegen_target == JavaScriptCodegenTarget::TypeScriptDeclarations {
100 "type "
101 } else {
102 ""
103 };
104 let alias_imports = concat(self.aliases.into_iter().sorted().map(|alias| {
105 docvec![
106 "import ",
107 import_modifier,
108 "* as ",
109 alias,
110 " from \"",
111 path.clone(),
112 r#"";"#,
113 line()
114 ]
115 }));
116 if self.unqualified.is_empty() {
117 alias_imports
118 } else {
119 let members = self.unqualified.into_iter().map(Member::into_doc);
120 let members = join(members, break_(",", ", "));
121 let members = docvec![
122 docvec![break_("", " "), members].nest(INDENT),
123 break_(",", " ")
124 ]
125 .group();
126 docvec![
127 alias_imports,
128 "import ",
129 import_modifier,
130 "{",
131 members,
132 "} from \"",
133 path,
134 r#"";"#,
135 line()
136 ]
137 }
138 }
139}
140
141#[derive(Debug)]
142pub struct Member<'a> {
143 pub name: Document<'a>,
144 pub alias: Option<Document<'a>>,
145}
146
147impl<'a> Member<'a> {
148 fn into_doc(self) -> Document<'a> {
149 match self.alias {
150 None => self.name,
151 Some(alias) => docvec![self.name, " as ", alias],
152 }
153 }
154}
155
156#[test]
157fn into_doc() {
158 let mut imports = Imports::new();
159 imports.register_module("./gleam/empty".into(), [], []);
160 imports.register_module(
161 "./multiple/times".into(),
162 ["wibble".into(), "wobble".into()],
163 [],
164 );
165 imports.register_module("./multiple/times".into(), ["wubble".into()], []);
166 imports.register_module(
167 "./multiple/times".into(),
168 [],
169 [Member {
170 name: "one".to_doc(),
171 alias: None,
172 }],
173 );
174
175 imports.register_module(
176 "./other".into(),
177 [],
178 [
179 Member {
180 name: "one".to_doc(),
181 alias: None,
182 },
183 Member {
184 name: "one".to_doc(),
185 alias: Some("onee".to_doc()),
186 },
187 Member {
188 name: "two".to_doc(),
189 alias: Some("twoo".to_doc()),
190 },
191 ],
192 );
193
194 imports.register_module(
195 "./other".into(),
196 [],
197 [
198 Member {
199 name: "three".to_doc(),
200 alias: None,
201 },
202 Member {
203 name: "four".to_doc(),
204 alias: None,
205 },
206 ],
207 );
208
209 imports.register_module(
210 "./zzz".into(),
211 [],
212 [
213 Member {
214 name: "one".to_doc(),
215 alias: None,
216 },
217 Member {
218 name: "two".to_doc(),
219 alias: None,
220 },
221 ],
222 );
223
224 assert_eq!(
225 line()
226 .append(imports.into_doc(JavaScriptCodegenTarget::JavaScript))
227 .to_pretty_string(40),
228 r#"
229import * as wibble from "./multiple/times";
230import * as wobble from "./multiple/times";
231import * as wubble from "./multiple/times";
232import { one } from "./multiple/times";
233import {
234 one,
235 one as onee,
236 two as twoo,
237 three,
238 four,
239} from "./other";
240import { one, two } from "./zzz";
241"#
242 .to_string()
243 );
244}