2024-03-31 13:23:08 +01:00
use std ::{ collections ::BTreeMap , fmt ::Display , fs ::read , path ::Path , str ::FromStr } ;
2024-03-04 20:18:49 +00:00
2024-03-26 22:39:58 +00:00
use cfg_if ::cfg_if ;
2024-03-04 20:18:49 +00:00
use relative_path ::RelativePathBuf ;
2024-03-24 13:31:11 +00:00
use semver ::Version ;
2024-03-04 20:18:49 +00:00
use serde ::{ Deserialize , Serialize } ;
use thiserror ::Error ;
2024-03-24 13:31:11 +00:00
use crate ::{
dependencies ::DependencySpecifier , package_name ::StandardPackageName , MANIFEST_FILE_NAME ,
} ;
2024-03-04 20:18:49 +00:00
/// The files exported by the package
#[ derive(Serialize, Deserialize, Debug, Clone, Default) ]
#[ serde(deny_unknown_fields) ]
pub struct Exports {
/// Points to the file which exports the package. As of currently this is only used for re-exporting types.
/// Libraries must have a structure in Roblox where the main file becomes the folder, for example:
/// A package called pesde/lib has a file called src/main.lua.
2024-03-24 13:31:11 +00:00
/// pesde puts this package in a folder called pesde_lib.
2024-03-04 20:18:49 +00:00
/// The package has to have set up configuration for file-syncing tools such as Rojo so that src/main.lua becomes the pesde_lib and turns it into a ModuleScript
#[ serde(default, skip_serializing_if = " Option::is_none " ) ]
pub lib : Option < RelativePathBuf > ,
/// Points to the file that will be executed with Lune
#[ serde(default, skip_serializing_if = " Option::is_none " ) ]
pub bin : Option < RelativePathBuf > ,
}
/// The path style used by the package
#[ derive(Serialize, Deserialize, Debug, Clone) ]
#[ serde(rename_all = " snake_case " , deny_unknown_fields) ]
pub enum PathStyle {
/// The path style used by Roblox (e.g. `script.Parent` or `script.Parent.Parent`)
Roblox {
/// A map of realm to in-game package folder location (used for linking between packages in different realms)
#[ serde(default) ]
place : BTreeMap < Realm , String > ,
} ,
}
impl Default for PathStyle {
fn default ( ) -> Self {
PathStyle ::Roblox {
place : BTreeMap ::new ( ) ,
}
}
}
impl Display for PathStyle {
fn fmt ( & self , f : & mut std ::fmt ::Formatter < '_ > ) -> std ::fmt ::Result {
match self {
PathStyle ::Roblox { .. } = > write! ( f , " roblox " ) ,
}
}
}
/// The realm of the package
#[ derive(
Serialize , Deserialize , Debug , Clone , PartialEq , Eq , Hash , PartialOrd , Ord , Copy , Default ,
) ]
#[ serde(rename_all = " snake_case " , deny_unknown_fields) ]
pub enum Realm {
/// The package is shared (usually ReplicatedStorage)
#[ default ]
Shared ,
/// The package is server only (usually ServerScriptService/ServerStorage)
Server ,
/// The package is development only
Development ,
}
impl Realm {
/// Returns the most restrictive realm
2024-03-25 16:29:31 +00:00
pub fn or ( self , other : Self ) -> Self {
2024-03-04 20:18:49 +00:00
match self {
Realm ::Shared = > other ,
_ = > self ,
}
}
}
impl Display for Realm {
fn fmt ( & self , f : & mut std ::fmt ::Formatter < '_ > ) -> std ::fmt ::Result {
match self {
Realm ::Shared = > write! ( f , " shared " ) ,
Realm ::Server = > write! ( f , " server " ) ,
Realm ::Development = > write! ( f , " development " ) ,
}
}
}
2024-03-10 18:03:12 +00:00
/// An error that occurred while parsing a realm from a string
#[ derive(Debug, Error) ]
#[ error( " invalid realm {0} " ) ]
pub struct FromStrRealmError ( String ) ;
impl FromStr for Realm {
type Err = FromStrRealmError ;
fn from_str ( s : & str ) -> Result < Self , Self ::Err > {
match s {
" shared " = > Ok ( Realm ::Shared ) ,
" server " = > Ok ( Realm ::Server ) ,
" development " = > Ok ( Realm ::Development ) ,
_ = > Err ( FromStrRealmError ( s . to_string ( ) ) ) ,
}
}
}
2024-03-26 22:39:58 +00:00
/// A key to override dependencies
#[ derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord) ]
pub struct OverrideKey ( pub Vec < Vec < String > > ) ;
impl Serialize for OverrideKey {
fn serialize < S : serde ::Serializer > ( & self , serializer : S ) -> Result < S ::Ok , S ::Error > {
serializer . serialize_str (
& self
. 0
. iter ( )
. map ( | overrides | {
overrides
. iter ( )
. map ( String ::to_string )
. collect ::< Vec < _ > > ( )
. join ( " > " )
} )
. collect ::< Vec < _ > > ( )
. join ( " , " ) ,
)
}
}
impl < ' de > Deserialize < ' de > for OverrideKey {
fn deserialize < D : serde ::Deserializer < ' de > > ( deserializer : D ) -> Result < Self , D ::Error > {
let s = String ::deserialize ( deserializer ) ? ;
let mut key = Vec ::new ( ) ;
for overrides in s . split ( ',' ) {
key . push (
overrides
. split ( '>' )
. map ( | s | String ::from_str ( s ) . map_err ( serde ::de ::Error ::custom ) )
. collect ::< Result < Vec < _ > , _ > > ( ) ? ,
) ;
}
Ok ( OverrideKey ( key ) )
}
}
2024-03-04 20:18:49 +00:00
/// The manifest of a package
#[ derive(Serialize, Deserialize, Debug, Clone) ]
pub struct Manifest {
/// The name of the package
2024-03-24 13:31:11 +00:00
pub name : StandardPackageName ,
2024-03-04 20:18:49 +00:00
/// The version of the package. Must be [semver](https://semver.org) compatible. The registry will not accept non-semver versions and the CLI will not handle such packages
pub version : Version ,
2024-03-25 16:29:31 +00:00
/// A short description of the package
#[ serde(default, skip_serializing_if = " Option::is_none " ) ]
pub description : Option < String > ,
/// The license of the package
#[ serde(default, skip_serializing_if = " Option::is_none " ) ]
pub license : Option < String > ,
/// The authors of the package
#[ serde(default, skip_serializing_if = " Option::is_none " ) ]
pub authors : Option < Vec < String > > ,
/// The repository of the package
#[ serde(default, skip_serializing_if = " Option::is_none " ) ]
pub repository : Option < String > ,
2024-03-04 20:18:49 +00:00
/// The files exported by the package
#[ serde(default) ]
pub exports : Exports ,
/// The path style to use for linking modules
#[ serde(default) ]
pub path_style : PathStyle ,
/// Whether the package is private (it should not be published)
#[ serde(default) ]
pub private : bool ,
/// The realm of the package
2024-03-31 13:23:08 +01:00
#[ serde(default, skip_serializing_if = " Option::is_none " ) ]
2024-03-04 20:18:49 +00:00
pub realm : Option < Realm > ,
2024-03-24 13:31:11 +00:00
/// Indices of the package
pub indices : BTreeMap < String , String > ,
2024-03-24 17:38:18 +00:00
/// The command to generate a `sourcemap.json`
#[ cfg(feature = " wally " ) ]
2024-03-31 13:23:08 +01:00
#[ serde(default, skip_serializing_if = " Option::is_none " ) ]
2024-03-24 17:38:18 +00:00
pub sourcemap_generator : Option < String > ,
2024-03-26 22:39:58 +00:00
/// Dependency overrides
#[ serde(default, skip_serializing_if = " BTreeMap::is_empty " ) ]
pub overrides : BTreeMap < OverrideKey , DependencySpecifier > ,
2024-03-04 20:18:49 +00:00
/// The dependencies of the package
2024-03-27 19:56:19 +00:00
#[ serde(default, skip_serializing_if = " BTreeMap::is_empty " ) ]
pub dependencies : BTreeMap < String , DependencySpecifier > ,
2024-03-04 20:18:49 +00:00
/// The peer dependencies of the package
2024-03-27 19:56:19 +00:00
#[ serde(default, skip_serializing_if = " BTreeMap::is_empty " ) ]
pub peer_dependencies : BTreeMap < String , DependencySpecifier > ,
2024-03-04 20:18:49 +00:00
}
/// An error that occurred while reading the manifest
#[ derive(Debug, Error) ]
pub enum ManifestReadError {
/// An error that occurred while interacting with the file system
#[ error( " error interacting with the file system " ) ]
Io ( #[ from ] std ::io ::Error ) ,
/// An error that occurred while deserializing the manifest
#[ error( " error deserializing manifest " ) ]
ManifestDeser ( #[ source ] serde_yaml ::Error ) ,
}
2024-03-24 13:31:11 +00:00
cfg_if! {
if #[ cfg(feature = " wally " ) ] {
/// An error that occurred while converting the manifest
#[ derive(Debug, Error) ]
pub enum ManifestConvertError {
/// An error that occurred while reading the manifest
#[ error( " error reading the manifest " ) ]
ManifestRead ( #[ from ] ManifestReadError ) ,
/// An error that occurred while converting the manifest
#[ error( " error converting the manifest " ) ]
ManifestConvert ( #[ source ] toml ::de ::Error ) ,
/// The given path does not have a parent
#[ error( " the path {0} does not have a parent " ) ]
NoParent ( std ::path ::PathBuf ) ,
/// An error that occurred while interacting with the file system
#[ error( " error interacting with the file system " ) ]
Io ( #[ from ] std ::io ::Error ) ,
/// An error that occurred while writing the manifest
#[ error( " error writing the manifest " ) ]
2024-03-31 13:23:08 +01:00
ManifestWrite ( #[ from ] crate ::manifest ::ManifestWriteError ) ,
2024-03-24 13:31:11 +00:00
/// An error that occurred while parsing the dependencies
#[ error( " error parsing the dependencies " ) ]
DependencyParse ( #[ from ] crate ::dependencies ::wally ::WallyManifestDependencyError ) ,
}
} else {
/// An error that occurred while converting the manifest
pub type ManifestConvertError = ManifestReadError ;
}
2024-03-16 21:36:12 +00:00
}
2024-03-31 13:23:08 +01:00
/// An error that occurred while writing the manifest
#[ derive(Debug, Error) ]
pub enum ManifestWriteError {
/// An error that occurred while interacting with the file system
#[ error( " error interacting with the file system " ) ]
Io ( #[ from ] std ::io ::Error ) ,
/// An error that occurred while serializing the manifest
#[ error( " error serializing manifest " ) ]
ManifestSer ( #[ from ] serde_yaml ::Error ) ,
}
2024-03-04 20:18:49 +00:00
/// The type of dependency
#[ derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, Default) ]
#[ serde(rename_all = " snake_case " ) ]
pub enum DependencyType {
/// A normal dependency
#[ default ]
Normal ,
/// A peer dependency
Peer ,
}
2024-03-31 13:23:08 +01:00
pub ( crate ) fn update_sync_tool_files ( project_path : & Path , name : String ) -> std ::io ::Result < ( ) > {
if let Ok ( file ) = std ::fs ::File ::open ( project_path . join ( " default.project.json " ) ) {
let mut project : serde_json ::Value = serde_json ::from_reader ( file ) ? ;
if project [ " name " ] . as_str ( ) = = Some ( & name ) {
return Ok ( ( ) ) ;
}
project [ " name " ] = serde_json ::Value ::String ( name ) ;
serde_json ::to_writer_pretty (
std ::fs ::File ::create ( project_path . join ( " default.project.json " ) ) ? ,
& project ,
) ? ;
}
Ok ( ( ) )
}
2024-03-04 20:18:49 +00:00
impl Manifest {
/// Reads a manifest from a path (if the path is a directory, it will look for the manifest file inside it, otherwise it will read the file directly)
pub fn from_path < P : AsRef < std ::path ::Path > > ( path : P ) -> Result < Self , ManifestReadError > {
let path = path . as_ref ( ) ;
let path = if path . file_name ( ) = = Some ( MANIFEST_FILE_NAME . as_ref ( ) ) {
path . to_path_buf ( )
} else {
path . join ( MANIFEST_FILE_NAME )
} ;
let raw_contents = read ( path ) ? ;
let manifest =
serde_yaml ::from_slice ( & raw_contents ) . map_err ( ManifestReadError ::ManifestDeser ) ? ;
Ok ( manifest )
}
2024-03-16 21:36:12 +00:00
/// Tries to read the manifest from the given path, and if it fails, tries converting the `wally.toml` and writes a `pesde.yaml` in the same directory
2024-03-24 13:31:11 +00:00
#[ cfg(feature = " wally " ) ]
2024-03-16 21:36:12 +00:00
pub fn from_path_or_convert < P : AsRef < std ::path ::Path > > (
path : P ,
) -> Result < Self , ManifestConvertError > {
let dir_path = if path . as_ref ( ) . file_name ( ) = = Some ( MANIFEST_FILE_NAME . as_ref ( ) ) {
path . as_ref ( )
. parent ( )
. ok_or_else ( | | ManifestConvertError ::NoParent ( path . as_ref ( ) . to_path_buf ( ) ) ) ?
. to_path_buf ( )
} else {
path . as_ref ( ) . to_path_buf ( )
} ;
Self ::from_path ( path ) . or_else ( | _ | {
let toml_path = dir_path . join ( " wally.toml " ) ;
2024-03-24 13:31:11 +00:00
let toml_contents = std ::fs ::read_to_string ( toml_path ) ? ;
let wally_manifest : crate ::dependencies ::wally ::WallyManifest =
2024-03-16 21:36:12 +00:00
toml ::from_str ( & toml_contents ) . map_err ( ManifestConvertError ::ManifestConvert ) ? ;
2024-03-24 13:31:11 +00:00
let dependencies =
crate ::dependencies ::wally ::parse_wally_dependencies ( wally_manifest . clone ( ) ) ? ;
2024-03-16 21:36:12 +00:00
let mut place = BTreeMap ::new ( ) ;
if let Some ( shared ) = wally_manifest . place . shared_packages {
if ! shared . is_empty ( ) {
place . insert ( Realm ::Shared , shared ) ;
}
}
if let Some ( server ) = wally_manifest . place . server_packages {
if ! server . is_empty ( ) {
place . insert ( Realm ::Server , server ) ;
}
}
let manifest = Self {
2024-03-31 16:45:07 +01:00
name : wally_manifest . package . name . clone ( ) . into ( ) ,
2024-03-16 21:36:12 +00:00
version : wally_manifest . package . version ,
2024-03-24 13:31:11 +00:00
exports : Exports {
lib : Some ( RelativePathBuf ::from ( " true " ) ) ,
bin : None ,
} ,
2024-03-16 21:36:12 +00:00
path_style : PathStyle ::Roblox { place } ,
private : wally_manifest . package . private . unwrap_or ( false ) ,
2024-03-31 13:23:08 +01:00
realm : wally_manifest . package . realm ,
2024-03-24 13:31:11 +00:00
indices : BTreeMap ::from ( [ (
crate ::project ::DEFAULT_INDEX_NAME . to_string ( ) ,
" " . to_string ( ) ,
) ] ) ,
2024-03-24 17:38:18 +00:00
sourcemap_generator : None ,
2024-03-26 22:39:58 +00:00
overrides : BTreeMap ::new ( ) ,
2024-03-24 17:38:18 +00:00
2024-03-24 13:31:11 +00:00
dependencies ,
2024-03-27 19:56:19 +00:00
peer_dependencies : Default ::default ( ) ,
2024-03-16 21:36:12 +00:00
description : wally_manifest . package . description ,
license : wally_manifest . package . license ,
authors : wally_manifest . package . authors ,
repository : None ,
} ;
2024-03-31 13:23:08 +01:00
manifest . write ( & dir_path ) ? ;
2024-03-31 16:45:07 +01:00
update_sync_tool_files ( & dir_path , wally_manifest . package . name . name ( ) . to_string ( ) ) ? ;
2024-03-16 21:36:12 +00:00
Ok ( manifest )
} )
}
2024-03-24 13:31:11 +00:00
/// Same as `from_path`, enable the `wally` feature to add support for converting `wally.toml` to `pesde.yaml`
#[ cfg(not(feature = " wally " )) ]
pub fn from_path_or_convert < P : AsRef < std ::path ::Path > > (
path : P ,
) -> Result < Self , ManifestReadError > {
Self ::from_path ( path )
}
2024-03-04 20:18:49 +00:00
/// Returns all dependencies
2024-03-27 19:56:19 +00:00
pub fn dependencies ( & self ) -> BTreeMap < String , ( DependencySpecifier , DependencyType ) > {
2024-03-04 20:18:49 +00:00
self . dependencies
. iter ( )
2024-03-27 19:56:19 +00:00
. map ( | ( desired_name , specifier ) | {
(
desired_name . clone ( ) ,
( specifier . clone ( ) , DependencyType ::Normal ) ,
)
} )
2024-03-04 20:18:49 +00:00
. chain (
self . peer_dependencies
. iter ( )
2024-03-27 19:56:19 +00:00
. map ( | ( desired_name , specifier ) | {
(
desired_name . clone ( ) ,
( specifier . clone ( ) , DependencyType ::Peer ) ,
)
} ) ,
2024-03-04 20:18:49 +00:00
)
. collect ( )
}
2024-03-31 13:23:08 +01:00
/// Writes the manifest to a path
pub fn write < P : AsRef < std ::path ::Path > > ( & self , to : P ) -> Result < ( ) , ManifestWriteError > {
let manifest_path = to . as_ref ( ) . join ( MANIFEST_FILE_NAME ) ;
serde_yaml ::to_writer ( std ::fs ::File ::create ( manifest_path ) ? , self ) ? ;
Ok ( ( ) )
}
2024-03-04 20:18:49 +00:00
}