pesde/src/linking_file.rs

285 lines
9.2 KiB
Rust
Raw Normal View History

2024-03-04 20:18:49 +00:00
use std::{
fs::{read, write},
iter,
path::{Component, Path, PathBuf},
str::from_utf8,
};
use full_moon::{
ast::types::ExportedTypeDeclaration,
parse,
visitors::{Visit, Visitor},
};
use log::debug;
use semver::Version;
use thiserror::Error;
use crate::{
dependencies::resolution::{packages_folder, ResolvedPackage, ResolvedVersionsMap},
index::Index,
manifest::{Manifest, ManifestReadError, PathStyle, Realm},
package_name::PackageName,
project::Project,
};
struct TypeVisitor {
pub(crate) types: Vec<String>,
}
impl Visitor for TypeVisitor {
fn visit_exported_type_declaration(&mut self, node: &ExportedTypeDeclaration) {
let name = node.type_declaration().type_name().to_string();
let (declaration_generics, generics) =
if let Some(declaration) = node.type_declaration().generics() {
let mut declaration_generics = vec![];
let mut generics = vec![];
for generic in declaration.generics().iter() {
declaration_generics.push(generic.to_string());
if generic.default_type().is_some() {
generics.push(generic.parameter().to_string())
} else {
generics.push(generic.to_string())
}
}
(
format!("<{}>", declaration_generics.join(", ")),
format!("<{}>", generics.join(", ")),
)
} else {
("".to_string(), "".to_string())
};
self.types.push(format!(
"export type {name}{declaration_generics} = module.{name}{generics}\n"
));
}
}
/// Generates the contents of a linking file, given the require path, and the contents of the target file
/// The contents will be scanned for type exports, and the linking file will be generated accordingly
pub fn linking_file(content: &str, path: &str) -> Result<String, full_moon::Error> {
let mut linker = format!("local module = require({path})\n");
let mut visitor = TypeVisitor { types: vec![] };
parse(content)?.nodes().visit(&mut visitor);
for ty in visitor.types {
linker.push_str(&ty);
}
linker.push_str("return module");
Ok(linker)
}
#[derive(Debug, Error)]
/// An error that occurred while linking dependencies
pub enum LinkingError {
#[error("error interacting with the file system")]
/// An error that occurred while interacting with the file system
Io(#[from] std::io::Error),
#[error("failed getting file name from {0}")]
/// An error that occurred while getting a file name
FileNameFail(PathBuf),
#[error("failed converting file name to string")]
/// An error that occurred while converting a file name to a string
FileNameToStringFail,
#[error("failed getting relative path from {0} to {1}")]
/// An error that occurred while getting a relative path
RelativePathFail(PathBuf, PathBuf),
#[error("failed getting path parent of {0}")]
/// An error that occurred while getting a path parent
ParentFail(PathBuf),
#[error("failed to convert path component to string")]
/// An error that occurred while converting a path component to a string
ComponentToStringFail,
#[error("failed to get path string")]
/// An error that occurred while getting a path string
PathToStringFail,
#[error("error encoding utf-8 string")]
/// An error that occurred while converting a byte slice to a string
Utf8(#[from] std::str::Utf8Error),
#[error("error reading manifest")]
/// An error that occurred while reading the manifest of a package
ManifestRead(#[from] ManifestReadError),
#[error("missing realm {0} in-game path")]
/// An error that occurred while getting the in-game path for a realm
MissingRealmInGamePath(Realm),
#[error("library source is not valid Luau")]
/// An error that occurred because the library source is not valid Luau
InvalidLuau(#[from] full_moon::Error),
}
pub(crate) fn link<P: AsRef<Path>, Q: AsRef<Path>, I: Index>(
project: &Project<I>,
resolved_pkg: &ResolvedPackage,
destination_dir: P,
parent_dependency_packages_dir: Q,
) -> Result<(), LinkingError> {
let (_, source_dir) = resolved_pkg.directory(project.path());
let file = Manifest::from_path(&source_dir)?;
let Some(lib_export) = file.exports.lib else {
return Ok(());
};
let lib_export = lib_export.to_path(&source_dir);
let path_style = &project.manifest().path_style;
let PathStyle::Roblox { place } = &path_style;
debug!("linking {resolved_pkg} using `{}` path style", path_style);
let name = resolved_pkg.pkg_ref.name().name();
let destination_dir = if resolved_pkg.is_root {
project.path().join(packages_folder(
&resolved_pkg.specifier.realm().cloned().unwrap_or_default(),
))
} else {
destination_dir.as_ref().to_path_buf()
};
let destination_file = destination_dir.join(format!("{name}.lua"));
let realm_folder = project.path().join(resolved_pkg.packages_folder());
let in_different_folders = realm_folder != parent_dependency_packages_dir.as_ref();
let mut path = if in_different_folders {
pathdiff::diff_paths(&source_dir, &realm_folder)
.ok_or_else(|| LinkingError::RelativePathFail(source_dir.clone(), realm_folder))?
} else {
pathdiff::diff_paths(&source_dir, &destination_dir).ok_or_else(|| {
LinkingError::RelativePathFail(source_dir.clone(), destination_dir.to_path_buf())
})?
};
path.set_extension("");
let beginning = if in_different_folders {
place
.get(&resolved_pkg.realm)
.ok_or_else(|| LinkingError::MissingRealmInGamePath(resolved_pkg.realm))?
.clone()
} else if name == "init" {
"script".to_string()
} else {
"script.Parent".to_string()
};
let path = iter::once(Ok(beginning))
.chain(path.components().map(|component| {
Ok(match component {
Component::ParentDir => ".Parent".to_string(),
Component::Normal(part) => format!(
"[{:?}]",
part.to_str().ok_or(LinkingError::ComponentToStringFail)?
),
_ => unreachable!("invalid path component"),
})
}))
.collect::<Result<String, LinkingError>>()?;
debug!(
"writing linking file for {} with import `{path}` to {}",
source_dir.display(),
destination_file.display()
);
let raw_file_contents = read(lib_export)?;
let file_contents = from_utf8(&raw_file_contents)?;
let linking_file_contents = linking_file(file_contents, &path)?;
write(&destination_file, linking_file_contents)?;
Ok(())
}
#[derive(Debug, Error)]
#[error("error linking {1}@{2} to {3}@{4}")]
/// An error that occurred while linking the dependencies
pub struct LinkingDependenciesError(
#[source] LinkingError,
PackageName,
Version,
PackageName,
Version,
);
impl<I: Index> Project<I> {
/// Links the dependencies of the project
pub fn link_dependencies(
&self,
map: &ResolvedVersionsMap,
) -> Result<(), LinkingDependenciesError> {
for (name, versions) in map {
for (version, resolved_pkg) in versions {
let (container_dir, _) = resolved_pkg.directory(self.path());
debug!(
"linking package {name}@{version}'s dependencies to directory {}",
container_dir.display()
);
for (dep_name, dep_version) in &resolved_pkg.dependencies {
let dep = map
.get(dep_name)
.and_then(|versions| versions.get(dep_version))
.unwrap();
link(
self,
dep,
&container_dir,
&self.path().join(resolved_pkg.packages_folder()),
)
.map_err(|e| {
LinkingDependenciesError(
e,
dep_name.clone(),
dep_version.clone(),
name.clone(),
version.clone(),
)
})?;
}
if resolved_pkg.is_root {
let linking_dir = &self.path().join(resolved_pkg.packages_folder());
debug!(
"linking root package {name}@{version} to directory {}",
linking_dir.display()
);
link(self, resolved_pkg, linking_dir, linking_dir).map_err(|e| {
LinkingDependenciesError(
e,
name.clone(),
version.clone(),
name.clone(),
version.clone(),
)
})?;
}
}
}
Ok(())
}
}