Fork of daniellemaywood.uk/gleam — Wasm codegen work
46 kB
1510 lines
1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2020 The Gleam contributors
3
4mod stale_package_remover;
5use crate::error::{FileIoAction, FileKind};
6use crate::io::FileSystemReader;
7use crate::io::ordered_map;
8use crate::manifest::Manifest;
9use crate::requirement::Requirement;
10use crate::version::COMPILER_VERSION;
11use crate::{Error, Result};
12use camino::{Utf8Path, Utf8PathBuf};
13use ecow::EcoString;
14use globset::{Glob, GlobSetBuilder};
15use hexpm::version::{self, LowestVersion, Version};
16use http::Uri;
17use regex::Regex;
18use serde::ser::SerializeSeq;
19use serde::{Deserialize, Serialize};
20use std::collections::HashMap;
21use std::fmt::{self};
22use std::marker::PhantomData;
23use std::sync::OnceLock;
24
25#[cfg(test)]
26use crate::manifest::ManifestPackage;
27
28use crate::build::{Mode, Runtime, Target};
29
30fn default_version() -> Version {
31 Version::parse("0.1.0").expect("default version")
32}
33
34fn erlang_target() -> Target {
35 Target::Erlang
36}
37
38fn default_javascript_runtime() -> Runtime {
39 Runtime::NodeJs
40}
41
42#[derive(Clone, Debug, PartialEq, Eq)]
43pub struct SpdxLicense {
44 pub licence: String,
45}
46
47impl<'de> Deserialize<'de> for SpdxLicense {
48 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
49 where
50 D: serde::Deserializer<'de>,
51 {
52 deserializer.deserialize_str(SpdxLicenseVisitor)
53 }
54}
55
56struct SpdxLicenseVisitor;
57
58impl<'de> serde::de::Visitor<'de> for SpdxLicenseVisitor {
59 type Value = SpdxLicense;
60
61 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
62 formatter.write_str("a SPDX License ID")
63 }
64
65 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
66 where
67 E: serde::de::Error,
68 {
69 match spdx::license_id(value) {
70 None => Err(serde::de::Error::custom(format!(
71 "{value} is not a known SPDX License identifier"
72 ))),
73 Some(_) => Ok(SpdxLicense {
74 licence: value.to_string(),
75 }),
76 }
77 }
78}
79
80impl Serialize for SpdxLicense {
81 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
82 where
83 S: serde::Serializer,
84 {
85 serializer.serialize_str(&self.licence)
86 }
87}
88
89impl AsRef<str> for SpdxLicense {
90 fn as_ref(&self) -> &str {
91 self.licence.as_str()
92 }
93}
94
95#[derive(Debug, PartialEq, Clone)]
96pub struct GleamVersion(version::Range);
97impl From<version::Range> for GleamVersion {
98 fn from(range: version::Range) -> Self {
99 Self(range)
100 }
101}
102
103impl From<GleamVersion> for version::Range {
104 fn from(gleam_version: GleamVersion) -> Self {
105 gleam_version.0
106 }
107}
108
109impl From<GleamVersion> for pubgrub::Range<Version> {
110 fn from(gleam_version: GleamVersion) -> Self {
111 gleam_version.0.into()
112 }
113}
114
115impl GleamVersion {
116 pub fn from_pubgrub(range: pubgrub::Range<Version>) -> Self {
117 let range: version::Range = range.into();
118 range.into()
119 }
120
121 pub fn as_pubgrub(&self) -> &pubgrub::Range<Version> {
122 self.0.to_pubgrub()
123 }
124
125 pub fn new(spec: String) -> Result<GleamVersion> {
126 let hex =
127 version::Range::new(spec.to_string()).map_err(|e| Error::InvalidVersionFormat {
128 input: spec,
129 error: e.to_string(),
130 })?;
131 Ok(hex.into())
132 }
133
134 pub fn lowest_version(&self) -> Option<Version> {
135 self.as_pubgrub().lowest_version()
136 }
137
138 pub fn hex(&self) -> &version::Range {
139 &self.0
140 }
141}
142
143#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
144pub struct PackageConfig {
145 #[serde(deserialize_with = "package_name::deserialize")]
146 pub name: EcoString,
147 #[serde(default = "default_version")]
148 pub version: Version,
149
150 #[serde(
151 default,
152 rename = "gleam",
153 deserialize_with = "deserialise_gleam_version",
154 serialize_with = "serialise_gleam_version"
155 )]
156 pub gleam_version: Option<GleamVersion>,
157 #[serde(default, alias = "licenses")]
158 pub licences: Vec<SpdxLicense>,
159 #[serde(default)]
160 pub description: EcoString,
161 #[serde(default, alias = "docs")]
162 pub documentation: Docs,
163 #[serde(
164 default,
165 serialize_with = "ordered_map",
166 deserialize_with = "map_with_package_name_keys::deserialize"
167 )]
168 pub dependencies: HashMap<EcoString, Requirement>,
169 #[serde(
170 default,
171 alias = "dev-dependencies",
172 serialize_with = "ordered_map",
173 deserialize_with = "map_with_package_name_keys::deserialize"
174 )]
175 pub dev_dependencies: HashMap<EcoString, Requirement>,
176 #[serde(default)]
177 pub repository: Option<Repository>,
178 #[serde(default)]
179 pub links: Vec<Link>,
180 #[serde(default)]
181 pub erlang: ErlangConfig,
182 #[serde(default)]
183 pub javascript: JavaScriptConfig,
184 #[serde(default = "erlang_target")]
185 pub target: Target,
186 #[serde(default)]
187 pub internal_modules: Option<Vec<Glob>>,
188}
189
190pub fn serialise_gleam_version<S>(
191 gleam_gersion: &Option<GleamVersion>,
192 serializer: S,
193) -> Result<S::Ok, S::Error>
194where
195 S: serde::Serializer,
196{
197 match gleam_gersion {
198 Some(version) => serializer.serialize_str(&version.hex().to_string()),
199 None => serializer.serialize_none(),
200 }
201}
202
203pub fn deserialise_gleam_version<'de, D>(deserialiser: D) -> Result<Option<GleamVersion>, D::Error>
204where
205 D: serde::Deserializer<'de>,
206{
207 match Deserialize::deserialize(deserialiser)? {
208 Some(range_string) => {
209 let hex = version::Range::new(range_string).map_err(serde::de::Error::custom)?;
210 Ok(Some(hex.into()))
211 }
212 None => Ok(None),
213 }
214}
215
216impl PackageConfig {
217 pub fn dependencies_for(&self, mode: Mode) -> Result<HashMap<EcoString, Requirement>> {
218 match mode {
219 Mode::Dev | Mode::Lsp => self.all_direct_dependencies(),
220 Mode::Prod => Ok(self.dependencies.clone()),
221 }
222 }
223
224 // Return all the dependencies listed in the configuration, that is, all the
225 // direct dependencies, both in the `dependencies` and `dev_dependencies`.
226 pub fn all_direct_dependencies(&self) -> Result<HashMap<EcoString, Requirement>> {
227 let mut deps =
228 HashMap::with_capacity(self.dependencies.len() + self.dev_dependencies.len());
229 for (name, requirement) in self.dependencies.iter().chain(&self.dev_dependencies) {
230 let already_inserted = deps.insert(name.clone(), requirement.clone()).is_some();
231 if already_inserted {
232 return Err(Error::DuplicateDependency(name.clone()));
233 }
234 }
235 Ok(deps)
236 }
237
238 pub fn read<FS: FileSystemReader, P: AsRef<Utf8Path>>(
239 path: P,
240 fs: &FS,
241 ) -> Result<PackageConfig, Error> {
242 let toml = fs.read(path.as_ref())?;
243 deserialise_config(path, toml)
244 }
245
246 /// Get the locked packages for the current config and a given (optional)
247 /// manifest of previously locked packages.
248 ///
249 /// If a package is removed or the specified required version range for it
250 /// changes then it is not considered locked. This also goes for any child
251 /// packages of the package which have no other parents.
252 ///
253 /// This function should be used each time resolution is performed so that
254 /// outdated deps are removed from the manifest and not locked to the
255 /// previously selected versions.
256 ///
257 pub fn locked(&self, manifest: Option<&Manifest>) -> Result<HashMap<EcoString, Version>> {
258 match manifest {
259 None => Ok(HashMap::new()),
260 Some(manifest) => {
261 let requirements = self.all_direct_dependencies()?;
262 let fresh_and_locked = stale_package_remover::StalePackageRemover::fresh_and_locked(
263 &requirements,
264 manifest,
265 );
266 Ok(fresh_and_locked)
267 }
268 }
269 }
270
271 /// Determines whether the given module should be hidden in the docs or not
272 ///
273 /// The developer can specify a list of glob patterns in the gleam.toml file
274 /// to determine modules that should not be shown in the package's documentation
275 pub fn is_internal_module(&self, module: &str) -> bool {
276 let package = &self.name;
277 match &self.internal_modules {
278 Some(globs) => {
279 let mut builder = GlobSetBuilder::new();
280 for glob in globs {
281 _ = builder.add(glob.clone());
282 }
283 builder.build()
284 }
285
286 // If no patterns were specified in the config then we use a default value
287 None => GlobSetBuilder::new()
288 .add(Glob::new(&format!("{package}/internal")).expect("internal module glob"))
289 .add(Glob::new(&format!("{package}/internal/*")).expect("internal module glob"))
290 .build(),
291 }
292 .expect("internal module globs")
293 .is_match(module)
294 }
295
296 // Checks to see if the gleam version specified in the config is compatible
297 // with the current compiler version
298 pub fn check_gleam_compatibility(&self) -> Result<(), Error> {
299 if let Some(version) = &self.gleam_version {
300 let range = version.as_pubgrub();
301 let compiler_version =
302 Version::parse(COMPILER_VERSION).expect("Parse compiler semantic version");
303
304 // We ignore the pre-release and build metadata when checking compatibility
305 let mut version_without_pre = compiler_version;
306 version_without_pre.pre = vec![];
307 version_without_pre.build = None;
308 if !range.contains(&version_without_pre) {
309 return Err(Error::IncompatibleCompilerVersion {
310 package: self.name.to_string(),
311 required_version: range.to_string(),
312 gleam_version: COMPILER_VERSION.to_string(),
313 });
314 }
315 }
316 Ok(())
317 }
318
319 pub fn tag_for_version(&self, version: &Version) -> String {
320 let prefix = match self.repository.as_ref() {
321 Some(
322 Repository::GitHub { tag_prefix, .. }
323 | Repository::GitLab { tag_prefix, .. }
324 | Repository::BitBucket { tag_prefix, .. }
325 | Repository::Codeberg { tag_prefix, .. }
326 | Repository::SourceHut { tag_prefix, .. }
327 | Repository::Gitea { tag_prefix, .. }
328 | Repository::Forgejo { tag_prefix, .. }
329 | Repository::Tangled { tag_prefix, .. },
330 ) => tag_prefix.as_ref(),
331
332 Some(Repository::Custom { .. }) | None => None,
333 };
334
335 match prefix {
336 Some(prefix) => format!("{prefix}v{version}"),
337 None => format!("v{version}"),
338 }
339 }
340}
341
342fn deserialise_config<P: AsRef<Utf8Path>>(
343 path: P,
344 toml: String,
345) -> std::result::Result<PackageConfig, Error> {
346 let config: PackageConfig = toml::from_str(&toml).map_err(|e| Error::FileIo {
347 action: FileIoAction::Parse,
348 kind: FileKind::File,
349 path: path.as_ref().to_path_buf(),
350 err: Some(e.to_string()),
351 })?;
352 Ok(config)
353}
354
355// https://github.com/gleam-lang/gleam/issues/4867
356#[test]
357fn deny_extra_deps_properties() {
358 let toml = r#"
359name = "wibble"
360version = "1.0.0"
361
362[dependencies]
363aide_generator = { git = "git@github.com:crowdhailer/aide.git", ref = "f559c5bc", extra = "idk what this is" }
364"#;
365 let error = deserialise_config("gleam.toml", toml.into())
366 .expect_err("should fail to deserialise because of additional path");
367
368 insta::assert_snapshot!(insta::internals::AutoName, error.pretty_string());
369}
370
371#[test]
372fn invalid_dependency_name() {
373 let toml = r#"
374name = "wibble"
375version = "1.0.0"
376
377[dependencies]
378Hello = "> 1.0.0"
379"#;
380 let error = deserialise_config("gleam.toml", toml.into())
381 .expect_err("should fail to deserialise because invalid name");
382 insta::assert_snapshot!(insta::internals::AutoName, error.pretty_string());
383}
384
385#[test]
386fn invalid_dev_dependency_name() {
387 let toml = r#"
388name = "wibble"
389version = "1.0.0"
390
391[dev_dependencies]
392"yes." = "> 1.0.0"
393"#;
394 let error = deserialise_config("gleam.toml", toml.into())
395 .expect_err("should fail to deserialise because invalid name");
396 insta::assert_snapshot!(insta::internals::AutoName, error.pretty_string());
397}
398
399#[test]
400fn locked_no_manifest() {
401 let mut config = PackageConfig::default();
402 config.dependencies = [
403 ("prod1".into(), Requirement::hex("~> 1.0").unwrap()),
404 ("prod2".into(), Requirement::hex("~> 2.0").unwrap()),
405 ]
406 .into();
407 config.dev_dependencies = [
408 ("dev1".into(), Requirement::hex("~> 1.0").unwrap()),
409 ("dev2".into(), Requirement::hex("~> 2.0").unwrap()),
410 ]
411 .into();
412 assert_eq!(config.locked(None).unwrap(), [].into());
413}
414
415#[test]
416fn locked_no_changes() {
417 let mut config = PackageConfig::default();
418 config.dependencies = [
419 ("prod1".into(), Requirement::hex("~> 1.0").unwrap()),
420 ("prod2".into(), Requirement::hex("~> 2.0").unwrap()),
421 ]
422 .into();
423 config.dev_dependencies = [
424 ("dev1".into(), Requirement::hex("~> 1.0").unwrap()),
425 ("dev2".into(), Requirement::hex("~> 2.0").unwrap()),
426 ]
427 .into();
428 let manifest = Manifest {
429 requirements: config.all_direct_dependencies().unwrap(),
430 packages: vec![
431 manifest_package("prod1", "1.1.0", &[]),
432 manifest_package("prod2", "1.2.0", &[]),
433 manifest_package("dev1", "1.1.0", &[]),
434 manifest_package("dev2", "1.2.0", &[]),
435 ],
436 };
437 assert_eq!(
438 config.locked(Some(&manifest)).unwrap(),
439 [
440 locked_version("prod1", "1.1.0"),
441 locked_version("prod2", "1.2.0"),
442 locked_version("dev1", "1.1.0"),
443 locked_version("dev2", "1.2.0"),
444 ]
445 .into()
446 );
447}
448
449#[test]
450fn locked_some_removed() {
451 let mut config = PackageConfig::default();
452 config.dependencies = [("prod1".into(), Requirement::hex("~> 1.0").unwrap())].into();
453 config.dev_dependencies = [("dev2".into(), Requirement::hex("~> 2.0").unwrap())].into();
454 let manifest = Manifest {
455 requirements: config.all_direct_dependencies().unwrap(),
456 packages: vec![
457 manifest_package("prod1", "1.1.0", &[]),
458 manifest_package("prod2", "1.2.0", &[]), // Not in config
459 manifest_package("dev1", "1.1.0", &[]), // Not in config
460 manifest_package("dev2", "1.2.0", &[]),
461 ],
462 };
463 assert_eq!(
464 config.locked(Some(&manifest)).unwrap(),
465 [
466 // prod2 removed
467 // dev1 removed
468 locked_version("prod1", "1.1.0"),
469 locked_version("dev2", "1.2.0"),
470 ]
471 .into()
472 );
473}
474
475#[test]
476fn locked_some_changed() {
477 let mut config = PackageConfig::default();
478 config.dependencies = [
479 ("prod1".into(), Requirement::hex("~> 3.0").unwrap()), // Does not match manifest
480 ("prod2".into(), Requirement::hex("~> 2.0").unwrap()),
481 ]
482 .into();
483 config.dev_dependencies = [
484 ("dev1".into(), Requirement::hex("~> 3.0").unwrap()), // Does not match manifest
485 ("dev2".into(), Requirement::hex("~> 2.0").unwrap()),
486 ]
487 .into();
488 let manifest = Manifest {
489 requirements: [
490 ("prod1".into(), Requirement::hex("~> 1.0").unwrap()),
491 ("prod2".into(), Requirement::hex("~> 2.0").unwrap()),
492 ("dev1".into(), Requirement::hex("~> 1.0").unwrap()),
493 ("dev2".into(), Requirement::hex("~> 2.0").unwrap()),
494 ]
495 .into(),
496 packages: vec![
497 manifest_package("prod1", "1.1.0", &[]),
498 manifest_package("prod2", "1.2.0", &[]),
499 manifest_package("dev1", "1.1.0", &[]),
500 manifest_package("dev2", "1.2.0", &[]),
501 ],
502 };
503 assert_eq!(
504 config.locked(Some(&manifest)).unwrap(),
505 [
506 // prod1 removed
507 // dev1 removed
508 locked_version("prod2", "1.2.0"),
509 locked_version("dev2", "1.2.0"),
510 ]
511 .into()
512 );
513}
514
515#[test]
516fn locked_nested_are_removed_too() {
517 let mut config = PackageConfig::default();
518 config.dependencies = [
519 ("1".into(), Requirement::hex("~> 2.0").unwrap()), // Does not match manifest
520 ("2".into(), Requirement::hex("~> 1.0").unwrap()),
521 ]
522 .into();
523 config.dev_dependencies = [].into();
524 let manifest = Manifest {
525 requirements: [
526 ("1".into(), Requirement::hex("~> 1.0").unwrap()),
527 ("2".into(), Requirement::hex("~> 1.0").unwrap()),
528 ]
529 .into(),
530 packages: vec![
531 manifest_package("1", "1.1.0", &["1.1", "1.2"]),
532 manifest_package("1.1", "1.1.0", &["1.1.1", "1.1.2"]),
533 manifest_package("1.1.1", "1.1.0", &["shared"]),
534 manifest_package("1.1.2", "1.1.0", &[]),
535 manifest_package("1.2", "1.1.0", &["1.2.1", "1.2.2"]),
536 manifest_package("1.2.1", "1.1.0", &[]),
537 manifest_package("1.2.2", "1.1.0", &[]),
538 manifest_package("2", "2.1.0", &["2.1", "2.2"]),
539 manifest_package("2.1", "2.1.0", &["2.1.1", "2.1.2"]),
540 manifest_package("2.1.1", "2.1.0", &[]),
541 manifest_package("2.1.2", "2.1.0", &[]),
542 manifest_package("2.2", "2.1.0", &["2.2.1", "2.2.2", "shared"]),
543 manifest_package("2.2.1", "2.1.0", &[]),
544 manifest_package("2.2.2", "2.1.0", &[]),
545 manifest_package("shared", "2.1.0", &[]),
546 ],
547 };
548 assert_eq!(
549 config.locked(Some(&manifest)).unwrap(),
550 [
551 // 1* removed
552 locked_version("2", "2.1.0"),
553 locked_version("2.1", "2.1.0"),
554 locked_version("2.1.1", "2.1.0"),
555 locked_version("2.1.2", "2.1.0"),
556 locked_version("2.2", "2.1.0"),
557 locked_version("2.2.1", "2.1.0"),
558 locked_version("2.2.2", "2.1.0"),
559 locked_version("shared", "2.1.0"),
560 ]
561 .into()
562 );
563}
564
565// https://github.com/gleam-lang/gleam/issues/1754
566#[test]
567fn locked_unlock_new() {
568 let mut config = PackageConfig::default();
569 config.dependencies = [
570 ("1".into(), Requirement::hex("~> 1.0").unwrap()),
571 ("2".into(), Requirement::hex("~> 1.0").unwrap()),
572 ("3".into(), Requirement::hex("~> 3.0").unwrap()), // Does not match manifest
573 ]
574 .into();
575 config.dev_dependencies = [].into();
576 let manifest = Manifest {
577 requirements: [
578 ("1".into(), Requirement::hex("~> 1.0").unwrap()),
579 ("2".into(), Requirement::hex("~> 1.0").unwrap()),
580 ]
581 .into(),
582 packages: vec![
583 manifest_package("1", "1.1.0", &["3"]),
584 manifest_package("2", "1.1.0", &["3"]),
585 manifest_package("3", "1.1.0", &[]),
586 ],
587 };
588 assert_eq!(
589 config.locked(Some(&manifest)).unwrap(),
590 [locked_version("1", "1.1.0"), locked_version("2", "1.1.0"),].into()
591 )
592}
593
594#[test]
595fn default_internal_modules() {
596 // When no internal modules are specified then we default to
597 // `["$package/internal", "$package/internal/*"]`
598 let mut config = PackageConfig::default();
599 config.name = "my_package".into();
600 config.internal_modules = None;
601
602 assert!(config.is_internal_module("my_package/internal"));
603 assert!(config.is_internal_module("my_package/internal/wibble"));
604 assert!(config.is_internal_module("my_package/internal/wibble/wobble"));
605 assert!(!config.is_internal_module("my_package/internallll"));
606 assert!(!config.is_internal_module("my_package/other"));
607 assert!(!config.is_internal_module("my_package/other/wibble"));
608 assert!(!config.is_internal_module("other/internal"));
609}
610
611#[test]
612fn no_internal_modules() {
613 // When no internal modules are specified then we default to
614 // `["$package/internal", "$package/internal/*"]`
615 let mut config = PackageConfig::default();
616 config.name = "my_package".into();
617 config.internal_modules = Some(vec![]);
618
619 assert!(!config.is_internal_module("my_package/internal"));
620 assert!(!config.is_internal_module("my_package/internal/wibble"));
621 assert!(!config.is_internal_module("my_package/internal/wibble/wobble"));
622 assert!(!config.is_internal_module("my_package/internallll"));
623 assert!(!config.is_internal_module("my_package/other"));
624 assert!(!config.is_internal_module("my_package/other/wibble"));
625 assert!(!config.is_internal_module("other/internal"));
626}
627
628#[test]
629fn hidden_a_directory_from_docs() {
630 let mut config = PackageConfig::default();
631 config.internal_modules = Some(vec![Glob::new("package/internal/*").expect("")]);
632
633 let mod1 = "package/internal";
634 let mod2 = "package/internal/module";
635
636 assert_eq!(config.is_internal_module(mod1), false);
637 assert_eq!(config.is_internal_module(mod2), true);
638}
639
640#[test]
641fn hidden_two_directories_from_docs() {
642 let mut config = PackageConfig::default();
643 config.internal_modules = Some(vec![
644 Glob::new("package/internal1/*").expect(""),
645 Glob::new("package/internal2/*").expect(""),
646 ]);
647
648 let mod1 = "package/internal1";
649 let mod2 = "package/internal1/module";
650 let mod3 = "package/internal2";
651 let mod4 = "package/internal2/module";
652
653 assert_eq!(config.is_internal_module(mod1), false);
654 assert_eq!(config.is_internal_module(mod2), true);
655 assert_eq!(config.is_internal_module(mod3), false);
656 assert_eq!(config.is_internal_module(mod4), true);
657}
658
659#[test]
660fn hidden_a_directory_and_a_file_from_docs() {
661 let mut config = PackageConfig::default();
662 config.internal_modules = Some(vec![
663 Glob::new("package/internal1/*").expect(""),
664 Glob::new("package/module").expect(""),
665 ]);
666
667 let mod1 = "package/internal1";
668 let mod2 = "package/internal1/module";
669 let mod3 = "package/module";
670 let mod4 = "package/module/inner";
671
672 assert_eq!(config.is_internal_module(mod1), false);
673 assert_eq!(config.is_internal_module(mod2), true);
674 assert_eq!(config.is_internal_module(mod3), true);
675 assert_eq!(config.is_internal_module(mod4), false);
676}
677
678#[test]
679fn hidden_a_file_in_all_directories_from_docs() {
680 let mut config = PackageConfig::default();
681 config.internal_modules = Some(vec![Glob::new("package/*/module1").expect("")]);
682
683 let mod1 = "package/internal1/module1";
684 let mod2 = "package/internal2/module1";
685 let mod3 = "package/internal2/module2";
686 let mod4 = "package/module";
687
688 assert_eq!(config.is_internal_module(mod1), true);
689 assert_eq!(config.is_internal_module(mod2), true);
690 assert_eq!(config.is_internal_module(mod3), false);
691 assert_eq!(config.is_internal_module(mod4), false);
692}
693
694#[cfg(test)]
695fn manifest_package(
696 name: &'static str,
697 version: &'static str,
698 requirements: &'static [&'static str],
699) -> ManifestPackage {
700 use crate::manifest::Base16Checksum;
701
702 ManifestPackage {
703 name: name.into(),
704 version: Version::parse(version).unwrap(),
705 build_tools: vec![],
706 otp_app: None,
707 requirements: requirements
708 .iter()
709 .map(|requirement| (*requirement).into())
710 .collect(),
711 source: crate::manifest::ManifestPackageSource::Hex {
712 outer_checksum: Base16Checksum(vec![]),
713 },
714 }
715}
716
717#[cfg(test)]
718fn locked_version(name: &'static str, version: &'static str) -> (EcoString, Version) {
719 (name.into(), Version::parse(version).unwrap())
720}
721
722impl Default for PackageConfig {
723 fn default() -> Self {
724 Self {
725 name: EcoString::new(),
726 version: default_version(),
727 gleam_version: None,
728 description: EcoString::new(),
729 documentation: Docs::default(),
730 dependencies: HashMap::new(),
731 erlang: ErlangConfig::default(),
732 javascript: JavaScriptConfig::default(),
733 repository: None,
734 dev_dependencies: HashMap::new(),
735 licences: vec![],
736 links: vec![],
737 internal_modules: None,
738 target: Target::Erlang,
739 }
740 }
741}
742
743#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Default, Clone)]
744pub struct ErlangConfig {
745 /// An module that can be set in the `.app` file as the entrypoint for a stateful application
746 /// that defines a singleton supervision tree.
747 /// Erlang syntax.
748 #[serde(default)]
749 pub application_start_module: Option<EcoString>,
750 /// The argument for the start module start function. If not set then `[]` is used as the
751 /// default argument.
752 /// Erlang syntax.
753 #[serde(default)]
754 pub application_start_argument: Option<EcoString>,
755 #[serde(default)]
756 pub extra_applications: Vec<EcoString>,
757}
758
759#[derive(Deserialize, Serialize, Debug, PartialEq, Default, Clone)]
760pub struct JavaScriptConfig {
761 #[serde(default)]
762 pub typescript_declarations: bool,
763 #[serde(default)]
764 pub source_maps: bool,
765 #[serde(default = "default_javascript_runtime")]
766 pub runtime: Runtime,
767 #[serde(default, rename = "deno")]
768 pub deno: DenoConfig,
769}
770
771#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
772pub enum DenoFlag {
773 AllowAll,
774 Allow(Vec<String>),
775}
776
777impl Default for DenoFlag {
778 fn default() -> Self {
779 Self::Allow(Vec::new())
780 }
781}
782
783impl Serialize for DenoFlag {
784 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
785 where
786 S: serde::Serializer,
787 {
788 match self {
789 DenoFlag::AllowAll => serializer.serialize_bool(true),
790 DenoFlag::Allow(items) => {
791 let mut seq = serializer.serialize_seq(Some(items.len()))?;
792 for e in items {
793 seq.serialize_element(e)?;
794 }
795 seq.end()
796 }
797 }
798 }
799}
800
801fn bool_or_seq_string_to_deno_flag<'de, D>(deserializer: D) -> Result<DenoFlag, D::Error>
802where
803 D: serde::Deserializer<'de>,
804{
805 struct StringOrVec(PhantomData<Vec<String>>);
806
807 impl<'de> serde::de::Visitor<'de> for StringOrVec {
808 type Value = DenoFlag;
809
810 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
811 formatter.write_str("bool or list of strings")
812 }
813
814 fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
815 where
816 E: serde::de::Error,
817 {
818 if value {
819 Ok(DenoFlag::AllowAll)
820 } else {
821 Ok(DenoFlag::default())
822 }
823 }
824
825 fn visit_seq<S>(self, visitor: S) -> Result<Self::Value, S::Error>
826 where
827 S: serde::de::SeqAccess<'de>,
828 {
829 let allow: Vec<String> =
830 Deserialize::deserialize(serde::de::value::SeqAccessDeserializer::new(visitor))
831 .unwrap_or_default();
832
833 Ok(DenoFlag::Allow(allow))
834 }
835 }
836
837 deserializer.deserialize_any(StringOrVec(PhantomData))
838}
839
840#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Default, Clone)]
841pub struct DenoConfig {
842 #[serde(default, deserialize_with = "bool_or_seq_string_to_deno_flag")]
843 pub allow_env: DenoFlag,
844 #[serde(default)]
845 pub allow_sys: bool,
846 #[serde(default)]
847 pub allow_hrtime: bool,
848 #[serde(default, deserialize_with = "bool_or_seq_string_to_deno_flag")]
849 pub allow_net: DenoFlag,
850 #[serde(default)]
851 pub allow_ffi: bool,
852 #[serde(default, deserialize_with = "bool_or_seq_string_to_deno_flag")]
853 pub allow_read: DenoFlag,
854 #[serde(default, deserialize_with = "bool_or_seq_string_to_deno_flag")]
855 pub allow_run: DenoFlag,
856 #[serde(default, deserialize_with = "bool_or_seq_string_to_deno_flag")]
857 pub allow_write: DenoFlag,
858 #[serde(default)]
859 pub allow_all: bool,
860 #[serde(default)]
861 pub unstable: bool,
862 #[serde(
863 default,
864 serialize_with = "uri_serde::serialize_option",
865 deserialize_with = "uri_serde::deserialize_option"
866 )]
867 pub location: Option<Uri>,
868}
869
870#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)]
871#[serde(tag = "type")]
872pub enum Repository {
873 #[serde(rename = "github")]
874 GitHub {
875 user: String,
876 repo: String,
877 path: Option<String>,
878 #[serde(alias = "tag-prefix")]
879 tag_prefix: Option<String>,
880 },
881 #[serde(rename = "gitlab")]
882 GitLab {
883 user: String,
884 repo: String,
885 path: Option<String>,
886 #[serde(alias = "tag-prefix")]
887 tag_prefix: Option<String>,
888 },
889 #[serde(rename = "bitbucket")]
890 BitBucket {
891 user: String,
892 repo: String,
893 path: Option<String>,
894 #[serde(alias = "tag-prefix")]
895 tag_prefix: Option<String>,
896 },
897 #[serde(rename = "codeberg")]
898 Codeberg {
899 user: String,
900 repo: String,
901 path: Option<String>,
902 #[serde(alias = "tag-prefix")]
903 tag_prefix: Option<String>,
904 },
905 #[serde(rename = "gitea")]
906 Gitea {
907 user: String,
908 repo: String,
909 path: Option<String>,
910 #[serde(alias = "tag-prefix")]
911 tag_prefix: Option<String>,
912 #[serde(
913 serialize_with = "uri_serde::serialize",
914 deserialize_with = "uri_serde_default_https::deserialize"
915 )]
916 host: Uri,
917 },
918 #[serde(rename = "forgejo")]
919 Forgejo {
920 user: String,
921 repo: String,
922 path: Option<String>,
923 #[serde(alias = "tag-prefix")]
924 tag_prefix: Option<String>,
925 #[serde(
926 serialize_with = "uri_serde::serialize",
927 deserialize_with = "uri_serde_default_https::deserialize"
928 )]
929 host: Uri,
930 },
931 #[serde(rename = "sourcehut")]
932 SourceHut {
933 user: String,
934 repo: String,
935 path: Option<String>,
936 #[serde(alias = "tag-prefix")]
937 tag_prefix: Option<String>,
938 },
939 #[serde(rename = "tangled")]
940 Tangled {
941 user: String,
942 repo: String,
943 path: Option<String>,
944 #[serde(alias = "tag-prefix")]
945 tag_prefix: Option<String>,
946 },
947 #[serde(rename = "custom")]
948 Custom {
949 url: String,
950 #[serde(alias = "tag-prefix")]
951 tag_prefix: Option<String>,
952 },
953}
954
955impl Repository {
956 pub fn url(&self) -> String {
957 match self {
958 Repository::GitHub { repo, user, .. } => {
959 format!("https://github.com/{user}/{repo}")
960 }
961 Repository::GitLab { repo, user, .. } => {
962 format!("https://gitlab.com/{user}/{repo}")
963 }
964 Repository::BitBucket { repo, user, .. } => {
965 format!("https://bitbucket.com/{user}/{repo}")
966 }
967 Repository::Codeberg { repo, user, .. } => {
968 format!("https://codeberg.org/{user}/{repo}")
969 }
970 Repository::SourceHut { repo, user, .. } => {
971 format!("https://git.sr.ht/~{user}/{repo}")
972 }
973 Repository::Tangled { repo, user, .. } => {
974 format!("https://tangled.sh/{user}/{repo}")
975 }
976 Repository::Gitea {
977 repo, user, host, ..
978 }
979 | Repository::Forgejo {
980 repo, user, host, ..
981 } => {
982 let string_host = host.to_string();
983 let cleaned_host = string_host.trim_end_matches('/');
984 format!("{cleaned_host}/{user}/{repo}")
985 }
986 Repository::Custom { url, .. } => url.clone(),
987 }
988 }
989
990 pub fn path(&self) -> Option<&String> {
991 match self {
992 Repository::GitHub { path, .. }
993 | Repository::GitLab { path, .. }
994 | Repository::BitBucket { path, .. }
995 | Repository::Codeberg { path, .. }
996 | Repository::SourceHut { path, .. }
997 | Repository::Tangled { path, .. }
998 | Repository::Gitea { path, .. }
999 | Repository::Forgejo { path, .. } => path.as_ref(),
1000
1001 Repository::Custom { .. } => None,
1002 }
1003 }
1004}
1005
1006#[derive(Deserialize, Serialize, Default, Debug, PartialEq, Eq, Clone)]
1007pub struct Docs {
1008 #[serde(default)]
1009 pub pages: Vec<DocsPage>,
1010}
1011
1012#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)]
1013pub struct DocsPage {
1014 pub title: String,
1015 #[serde(default, deserialize_with = "package_scoped_path::deserialize")]
1016 pub path: Utf8PathBuf,
1017 #[serde(default, deserialize_with = "package_scoped_path::deserialize")]
1018 pub source: Utf8PathBuf,
1019}
1020
1021pub(crate) mod package_scoped_path {
1022 use camino::Utf8PathBuf;
1023 use serde::{Deserialize, Deserializer, de::Error as _};
1024
1025 pub fn deserialize<'de, D>(deserializer: D) -> Result<Utf8PathBuf, D::Error>
1026 where
1027 D: Deserializer<'de>,
1028 {
1029 let path = Utf8PathBuf::deserialize(deserializer)?;
1030 crate::io::validate_safe_relative_path(&path).map_err(D::Error::custom)?;
1031 Ok(path)
1032 }
1033
1034 pub fn optional_deserialize<'de, D>(deserializer: D) -> Result<Option<Utf8PathBuf>, D::Error>
1035 where
1036 D: Deserializer<'de>,
1037 {
1038 let path = Option::<Utf8PathBuf>::deserialize(deserializer)?;
1039 if let Some(path) = &path {
1040 crate::io::validate_safe_relative_path(path).map_err(D::Error::custom)?;
1041 }
1042 Ok(path)
1043 }
1044}
1045
1046#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)]
1047pub struct Link {
1048 pub title: String,
1049 #[serde(with = "uri_serde")]
1050 pub href: Uri,
1051}
1052
1053// Note we don't use http-serde since we also want to validate the scheme and host is set.
1054mod uri_serde {
1055 use http::uri::InvalidUri;
1056 use serde::{Deserialize, Deserializer, de::Error as _};
1057
1058 pub fn deserialize<'de, D>(deserializer: D) -> Result<http::Uri, D::Error>
1059 where
1060 D: Deserializer<'de>,
1061 {
1062 let string = String::deserialize(deserializer)?;
1063 let uri: http::Uri = string
1064 .parse()
1065 .map_err(|err: InvalidUri| D::Error::custom(err.to_string()))?;
1066 if uri.scheme().is_none() || uri.host().is_none() {
1067 return Err(D::Error::custom("uri without scheme"));
1068 }
1069 Ok(uri)
1070 }
1071
1072 pub fn deserialize_option<'de, D>(deserializer: D) -> Result<Option<http::Uri>, D::Error>
1073 where
1074 D: Deserializer<'de>,
1075 {
1076 let string: Option<String> = Option::deserialize(deserializer)?;
1077 match string {
1078 Some(s) => {
1079 let deserializer = serde::de::value::StringDeserializer::new(s);
1080 deserialize(deserializer).map(Some)
1081 }
1082 None => Ok(None),
1083 }
1084 }
1085
1086 pub fn serialize_option<S>(uri: &Option<http::Uri>, serializer: S) -> Result<S::Ok, S::Error>
1087 where
1088 S: serde::Serializer,
1089 {
1090 match uri {
1091 Some(uri) => serialize(uri, serializer),
1092 None => serializer.serialize_unit(),
1093 }
1094 }
1095
1096 pub fn serialize<S>(uri: &http::Uri, serializer: S) -> Result<S::Ok, S::Error>
1097 where
1098 S: serde::Serializer,
1099 {
1100 serializer.serialize_str(&uri.to_string())
1101 }
1102}
1103
1104// This prefixes https as a default in the event no scheme was provided
1105mod uri_serde_default_https {
1106 use http::uri::InvalidUri;
1107 use serde::{Deserialize, Deserializer, de::Error as _};
1108
1109 pub fn deserialize<'de, D>(deserializer: D) -> Result<http::Uri, D::Error>
1110 where
1111 D: Deserializer<'de>,
1112 {
1113 let string = String::deserialize(deserializer)?;
1114 let uri: http::Uri = string
1115 .parse()
1116 .map_err(|err: InvalidUri| D::Error::custom(err.to_string()))?;
1117 if uri.host().is_none() {
1118 return Err(D::Error::custom("uri without host"));
1119 }
1120 match uri.scheme().is_none() {
1121 true => format!("https://{string}")
1122 .parse()
1123 .map_err(|err: InvalidUri| D::Error::custom(err.to_string())),
1124 false => Ok(uri),
1125 }
1126 }
1127}
1128
1129pub(crate) mod package_name {
1130 use ecow::EcoString;
1131 use serde::Deserializer;
1132 use std::fmt;
1133
1134 pub fn deserialize<'de, D>(deserializer: D) -> Result<EcoString, D::Error>
1135 where
1136 D: Deserializer<'de>,
1137 {
1138 deserializer.deserialize_str(NameVisitor)
1139 }
1140
1141 struct NameVisitor;
1142
1143 impl<'de> serde::de::Visitor<'de> for NameVisitor {
1144 type Value = EcoString;
1145
1146 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1147 formatter.write_str("a package name")
1148 }
1149
1150 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
1151 where
1152 E: serde::de::Error,
1153 {
1154 if super::is_valid_package_name(value) {
1155 Ok(value.into())
1156 } else {
1157 let error =
1158 "Package names may only contain lowercase letters, numbers, and underscores";
1159 Err(serde::de::Error::custom(error))
1160 }
1161 }
1162 }
1163
1164 pub fn optional_deserialize<'de, D>(deserializer: D) -> Result<Option<EcoString>, D::Error>
1165 where
1166 D: Deserializer<'de>,
1167 {
1168 deserializer.deserialize_option(OptionVisitor)
1169 }
1170
1171 struct OptionVisitor;
1172
1173 impl<'de> serde::de::Visitor<'de> for OptionVisitor {
1174 type Value = Option<EcoString>;
1175
1176 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1177 formatter.write_str("an optional package name")
1178 }
1179
1180 fn visit_none<E>(self) -> Result<Self::Value, E>
1181 where
1182 E: serde::de::Error,
1183 {
1184 Ok(None)
1185 }
1186
1187 fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
1188 where
1189 D: Deserializer<'de>,
1190 {
1191 deserialize(deserializer).map(Some)
1192 }
1193 }
1194}
1195
1196pub mod map_with_package_name_keys {
1197 use ecow::EcoString;
1198 use serde::{Deserialize, Deserializer, de};
1199 use std::collections::HashMap;
1200
1201 pub fn deserialize<'de, D, V>(deserializer: D) -> Result<HashMap<EcoString, V>, D::Error>
1202 where
1203 D: Deserializer<'de>,
1204 V: Deserialize<'de>,
1205 {
1206 struct DependenciesVisitor<V>(std::marker::PhantomData<V>);
1207
1208 impl<'de, V: Deserialize<'de>> de::Visitor<'de> for DependenciesVisitor<V> {
1209 type Value = HashMap<EcoString, V>;
1210
1211 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1212 write!(f, "a map with valid package name keys")
1213 }
1214
1215 fn visit_map<A: de::MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
1216 let mut dependencies = HashMap::new();
1217 while let Some(key) = map.next_key::<EcoString>()? {
1218 if !super::is_valid_package_name(&key) {
1219 return Err(de::Error::invalid_value(
1220 de::Unexpected::Str(&key),
1221 &"a package name containing only lowercase letter, numbers, and underscores",
1222 ));
1223 }
1224 let _: Option<_> = dependencies.insert(key, map.next_value()?);
1225 }
1226 Ok(dependencies)
1227 }
1228 }
1229
1230 deserializer.deserialize_map(DependenciesVisitor(std::marker::PhantomData))
1231 }
1232}
1233
1234static PACKAGE_NAME_PATTERN: OnceLock<Regex> = OnceLock::new();
1235
1236fn is_valid_package_name(name: &str) -> bool {
1237 PACKAGE_NAME_PATTERN
1238 .get_or_init(|| Regex::new("^[a-z][a-z0-9_]*$").expect("Package name regex"))
1239 .is_match(name)
1240}
1241
1242#[test]
1243fn name_with_dash() {
1244 let input = r#"
1245name = "one-two"
1246"#;
1247
1248 insta::assert_snapshot!(
1249 insta::internals::AutoName,
1250 toml::from_str::<PackageConfig>(input)
1251 .unwrap_err()
1252 .to_string()
1253 );
1254}
1255
1256#[test]
1257fn name_with_number_start() {
1258 let input = r#"
1259name = "1"
1260"#;
1261 insta::assert_snapshot!(
1262 insta::internals::AutoName,
1263 toml::from_str::<PackageConfig>(input)
1264 .unwrap_err()
1265 .to_string(),
1266 )
1267}
1268
1269#[test]
1270fn docs_dot_dot_string_path() {
1271 let input = r#"
1272name = "one_two"
1273
1274[documentation]
1275pages = [{ title = "My Page", path = "../secrets.txt", source = "./path/to/my-page.md" }]
1276"#;
1277
1278 insta::assert_snapshot!(
1279 insta::internals::AutoName,
1280 toml::from_str::<PackageConfig>(input)
1281 .unwrap_err()
1282 .to_string()
1283 );
1284}
1285
1286#[test]
1287fn docs_prefix_dot_dot_string_path() {
1288 let input = r#"
1289name = "one_two"
1290
1291[documentation]
1292pages = [{ title = "My Page", path = "something/../../secrets.txt", source = "./path/to/my-page.md" }]
1293"#;
1294
1295 insta::assert_snapshot!(
1296 insta::internals::AutoName,
1297 toml::from_str::<PackageConfig>(input)
1298 .unwrap_err()
1299 .to_string()
1300 );
1301}
1302
1303#[cfg(windows)]
1304#[test]
1305fn docs_windows_dot_dot_string_path() {
1306 let input = r#"
1307name = "one_two"
1308
1309[documentation]
1310pages = [{ title = "My Page", path = "..\\secrets.txt", source = "./path/to/my-page.md" }]
1311"#;
1312
1313 insta::assert_snapshot!(
1314 insta::internals::AutoName,
1315 toml::from_str::<PackageConfig>(input)
1316 .unwrap_err()
1317 .to_string()
1318 );
1319}
1320
1321#[cfg(windows)]
1322#[test]
1323fn docs_prefix_windows_dot_dot_string_path() {
1324 let input = r#"
1325name = "one_two"
1326
1327[documentation]
1328pages = [{ title = "My Page", path = "something\\..\\..\\secrets.txt", source = "./path/to/my-page.md" }]
1329"#;
1330
1331 insta::assert_snapshot!(
1332 insta::internals::AutoName,
1333 toml::from_str::<PackageConfig>(input)
1334 .unwrap_err()
1335 .to_string()
1336 );
1337}
1338
1339#[test]
1340fn docs_dot_dot_path_buf() {
1341 let input = r#"
1342name = "one_two"
1343
1344[documentation]
1345pages = [{ title = "My Page", path = "stuff.html", source = "../secrets.txt" }]
1346"#;
1347
1348 insta::assert_snapshot!(
1349 insta::internals::AutoName,
1350 toml::from_str::<PackageConfig>(input)
1351 .unwrap_err()
1352 .to_string()
1353 );
1354}
1355
1356#[test]
1357fn docs_prefix_dot_dot_path_buf() {
1358 let input = r#"
1359name = "one_two"
1360
1361[documentation]
1362pages = [{ title = "My Page", path = "stuff.html", source = "something/../../secrets.txt" }]
1363"#;
1364
1365 insta::assert_snapshot!(
1366 insta::internals::AutoName,
1367 toml::from_str::<PackageConfig>(input)
1368 .unwrap_err()
1369 .to_string()
1370 );
1371}
1372
1373#[cfg(windows)]
1374#[test]
1375fn docs_windows_dot_dot_path_buf() {
1376 let input = r#"
1377name = "one_two"
1378
1379[documentation]
1380pages = [{ title = "My Page", path = "stuff.html", source = "..\\secrets.txt" }]
1381"#;
1382
1383 assert!(toml::from_str::<PackageConfig>(input).is_err())
1384}
1385
1386#[cfg(windows)]
1387#[test]
1388fn docs_prefix_windows_dot_dot_path_buf() {
1389 let input = r#"
1390name = "one_two"
1391
1392[documentation]
1393pages = [{ title = "My Page", path = "stuff.html", source = "something\\..\\..\\secrets.txt" }]
1394"#;
1395
1396 assert!(toml::from_str::<PackageConfig>(input).is_err())
1397}
1398
1399#[cfg(windows)]
1400#[test]
1401fn docs_absolute_source_windows() {
1402 let input = r#"
1403name = "one_two"
1404
1405[documentation]
1406pages = [{ title = "My Page", path = "stuff.html", source = "C:\\etc\\passwd" }]
1407"#;
1408
1409 assert!(toml::from_str::<PackageConfig>(input).is_err())
1410}
1411
1412#[test]
1413fn docs_absolute_source() {
1414 let input = r#"
1415name = "one_two"
1416
1417[documentation]
1418pages = [{ title = "My Page", path = "stuff.html", source = "/etc/passwd" }]
1419"#;
1420
1421 insta::assert_snapshot!(
1422 insta::internals::AutoName,
1423 toml::from_str::<PackageConfig>(input)
1424 .unwrap_err()
1425 .to_string()
1426 );
1427}
1428
1429#[test]
1430fn package_config_to_json() {
1431 let input = r#"
1432name = "my_project"
1433version = "1.0.0"
1434licences = ["Apache-2.0", "MIT"]
1435description = "Pretty complex config"
1436target = "erlang"
1437repository = { type = "github", user = "example", repo = "my_dep" }
1438links = [{ title = "Home page", href = "https://example.com" }]
1439internal_modules = ["my_app/internal"]
1440gleam = ">= 0.30.0"
1441
1442[dependencies]
1443gleam_stdlib = ">= 0.18.0 and < 2.0.0"
1444my_other_project = { path = "../my_other_project" }
1445
1446[dev_dependencies]
1447gleeunit = ">= 1.0.0 and < 2.0.0"
1448
1449[documentation]
1450pages = [{ title = "My Page", path = "my-page.html", source = "./path/to/my-page.md" }]
1451
1452[erlang]
1453application_start_module = "my_app/application"
1454extra_applications = ["inets", "ssl"]
1455
1456[javascript]
1457typescript_declarations = true
1458source_maps = true
1459runtime = "node"
1460
1461[javascript.deno]
1462allow_all = false
1463allow_ffi = true
1464allow_env = ["DATABASE_URL"]
1465allow_net = ["example.com:443"]
1466allow_read = ["./database.sqlite"]
1467"#;
1468
1469 let config = toml::from_str::<PackageConfig>(input).unwrap();
1470 let json = serde_json::to_string_pretty(&config).unwrap();
1471 let output = format!("--- GLEAM.TOML\n{input}\n\n--- EXPORTED JSON\n\n{json}");
1472 insta::assert_snapshot!(output);
1473
1474 let roundtrip = serde_json::from_str::<PackageConfig>(&json).unwrap();
1475 assert_eq!(config, roundtrip);
1476}
1477
1478#[test]
1479fn barebones_package_config_to_json() {
1480 let input = r#"
1481name = "my_project"
1482version = "1.0.0"
1483"#;
1484
1485 let config = toml::from_str::<PackageConfig>(input).unwrap();
1486 let json = serde_json::to_string_pretty(&config).unwrap();
1487 let output = format!("--- GLEAM.TOML\n{input}\n\n--- EXPORTED JSON\n\n{json}");
1488 insta::assert_snapshot!(output);
1489}
1490
1491#[test]
1492fn dev_deps_field_name() {
1493 let toml = r#"
1494name = "wibble"
1495version = "1.0.0"
1496
1497[dev_dependencies]
1498wibble = ">= 1.0.0 and < 2.0.0"
1499"#;
1500 let hyphen_alternative = deserialise_config("gleam.toml", toml.into()).expect("valid config");
1501 let toml = r#"
1502name = "wibble"
1503version = "1.0.0"
1504
1505[dev_dependencies]
1506wibble = ">= 1.0.0 and < 2.0.0"
1507"#;
1508 let canonical = deserialise_config("gleam.toml", toml.into()).expect("valid config");
1509 assert_eq!(canonical, hyphen_alternative)
1510}