Updated dependencies + A few other things (!150)
Initially this was going to be an update to dependencies but it seems i got a little carried away!
Anyways this PR removes 2 unused dependencies (`jshint` and `utf-8-validate`), and 2 other, `del` and `fs-extra` that were replaced by the built-in `fs/promises`.
It also renames all `tsconfig` and `Dockerfile` files, in a way that when viewed in a file tree sorted alphabetically they will be next to each other.
It also updates the typescript target to `ES2022`, and changes moduleResolution from `Node` to `Node10` (this isn't an update, they are the same thing but `Node` is now deprecated).
It also adds the `node:` discriminator to every import from built-in modules.
It also has major changes to the build script, `del` and `fs-extra` were only being used in the build script, it's now using `fs/promises` instead, cleaned up the code from some functions, adds better documentation to a few functions, and renames some gulp tasks and npm scripts to better represent what they actually do.
And finally it updates dependencies, except for `atomically` which can't be updated unless the project switches to ESM.
Reviewed-on: https://dev.sp-tarkov.com/SPT-AKI/Server/pulls/150
Co-authored-by: TheSparta <thesparta@noreply.dev.sp-tarkov.com>
Co-committed-by: TheSparta <thesparta@noreply.dev.sp-tarkov.com>
2023-10-14 09:01:41 +00:00
import { execSync } from "node:child_process" ;
import os from "node:os" ;
import path from "node:path" ;
2023-03-03 15:23:46 +00:00
import semver from "semver" ;
import { DependencyContainer , inject , injectable } from "tsyringe" ;
2023-10-19 17:21:17 +00:00
import { ModLoadOrder } from "@spt-aki/loaders/ModLoadOrder" ;
import { ModTypeCheck } from "@spt-aki/loaders/ModTypeCheck" ;
import { ModDetails } from "@spt-aki/models/eft/profile/IAkiProfile" ;
import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes" ;
import { IPreAkiLoadMod } from "@spt-aki/models/external/IPreAkiLoadMod" ;
import { IPreAkiLoadModAsync } from "@spt-aki/models/external/IPreAkiLoadModAsync" ;
import { ICoreConfig } from "@spt-aki/models/spt/config/ICoreConfig" ;
import { IModLoader } from "@spt-aki/models/spt/mod/IModLoader" ;
import { IPackageJsonData } from "@spt-aki/models/spt/mod/IPackageJsonData" ;
import { ILogger } from "@spt-aki/models/spt/utils/ILogger" ;
import { ConfigServer } from "@spt-aki/servers/ConfigServer" ;
import { LocalisationService } from "@spt-aki/services/LocalisationService" ;
import { ModCompilerService } from "@spt-aki/services/ModCompilerService" ;
import { JsonUtil } from "@spt-aki/utils/JsonUtil" ;
import { VFS } from "@spt-aki/utils/VFS" ;
2023-03-03 15:23:46 +00:00
@injectable ( )
export class PreAkiModLoader implements IModLoader
{
2024-03-24 17:52:38 +00:00
protected container : DependencyContainer ;
2023-03-03 15:23:46 +00:00
protected readonly basepath = "user/mods/" ;
protected readonly modOrderPath = "user/mods/order.json" ;
protected order : Record < string , number > = { } ;
protected imported : Record < string , IPackageJsonData > = { } ;
protected akiConfig : ICoreConfig ;
2023-10-10 11:03:20 +00:00
protected serverDependencies : Record < string , string > ;
2023-11-06 14:51:31 +00:00
protected skippedMods : Set < string > ;
2023-03-03 15:23:46 +00:00
constructor (
@inject ( "WinstonLogger" ) protected logger : ILogger ,
@inject ( "VFS" ) protected vfs : VFS ,
@inject ( "JsonUtil" ) protected jsonUtil : JsonUtil ,
@inject ( "ModCompilerService" ) protected modCompilerService : ModCompilerService ,
@inject ( "LocalisationService" ) protected localisationService : LocalisationService ,
@inject ( "ConfigServer" ) protected configServer : ConfigServer ,
2023-10-18 14:44:29 +00:00
@inject ( "ModLoadOrder" ) protected modLoadOrder : ModLoadOrder ,
2023-11-16 21:42:06 +00:00
@inject ( "ModTypeCheck" ) protected modTypeCheck : ModTypeCheck ,
2023-03-03 15:23:46 +00:00
)
{
this . akiConfig = this . configServer . getConfig < ICoreConfig > ( ConfigTypes . CORE ) ;
2023-10-10 11:03:20 +00:00
const packageJsonPath : string = path . join ( __dirname , "../../package.json" ) ;
this . serverDependencies = JSON . parse ( this . vfs . readFile ( packageJsonPath ) ) . dependencies ;
2023-11-06 14:51:31 +00:00
this . skippedMods = new Set ( ) ;
2023-03-03 15:23:46 +00:00
}
public async load ( container : DependencyContainer ) : Promise < void >
{
if ( globalThis . G_MODS_ENABLED )
{
2024-03-24 17:52:38 +00:00
this . container = container ;
2023-10-20 12:23:19 +01:00
await this . importModsAsync ( ) ;
2024-03-24 17:52:38 +00:00
await this . executeModsAsync ( ) ;
2023-03-03 15:23:46 +00:00
}
}
/ * *
* Returns a list of mods with preserved load order
* @returns Array of mod names in load order
* /
public getImportedModsNames ( ) : string [ ]
{
return Object . keys ( this . imported ) ;
}
public getImportedModDetails ( ) : Record < string , IPackageJsonData >
{
return this . imported ;
}
2023-10-10 17:01:29 +01:00
public getProfileModsGroupedByModName ( profileMods : ModDetails [ ] ) : ModDetails [ ]
{
// Group all mods used by profile by name
const modsGroupedByName : Record < string , ModDetails [ ] > = { } ;
for ( const mod of profileMods )
{
if ( ! modsGroupedByName [ mod . name ] )
{
modsGroupedByName [ mod . name ] = [ ] ;
}
modsGroupedByName [ mod . name ] . push ( mod ) ;
}
// Find the highest versioned mod and add to results array
const result = [ ] ;
for ( const modName in modsGroupedByName )
{
const modDatas = modsGroupedByName [ modName ] ;
2023-11-16 21:42:06 +00:00
const modVersions = modDatas . map ( ( x ) = > x . version ) ;
2023-10-10 17:01:29 +01:00
const highestVersion = semver . maxSatisfying ( modVersions , "*" ) ;
2023-11-16 21:42:06 +00:00
const chosenVersion = modDatas . find ( ( x ) = > x . name === modName && x . version === highestVersion ) ;
2023-10-10 17:01:29 +01:00
if ( ! chosenVersion )
{
continue ;
}
result . push ( chosenVersion ) ;
}
return result ;
}
2023-03-03 15:23:46 +00:00
public getModPath ( mod : string ) : string
{
return ` ${ this . basepath } ${ mod } / ` ;
}
2023-10-20 12:23:19 +01:00
protected async importModsAsync ( ) : Promise < void >
2023-03-03 15:23:46 +00:00
{
if ( ! this . vfs . exists ( this . basepath ) )
{
// no mods folder found
this . logger . info ( this . localisationService . getText ( "modloader-user_mod_folder_missing" ) ) ;
this . vfs . createDir ( this . basepath ) ;
return ;
}
2023-11-06 14:51:31 +00:00
/ * *
* array of mod folder names
* /
const mods : string [ ] = this . vfs . getDirs ( this . basepath ) ;
2023-11-16 21:42:06 +00:00
2023-03-03 15:23:46 +00:00
this . logger . info ( this . localisationService . getText ( "modloader-loading_mods" , mods . length ) ) ;
// Mod order
2023-11-16 21:42:06 +00:00
if ( ! this . vfs . exists ( this . modOrderPath ) )
2023-03-03 15:23:46 +00:00
{
this . logger . info ( this . localisationService . getText ( "modloader-mod_order_missing" ) ) ;
2023-08-09 10:49:45 +00:00
// Write file with empty order array to disk
2023-11-16 21:42:06 +00:00
this . vfs . writeFile ( this . modOrderPath , this . jsonUtil . serializeAdvanced ( { order : [ ] } , null , 4 ) ) ;
2023-03-03 15:23:46 +00:00
}
2023-11-16 21:42:06 +00:00
else
2023-03-03 15:23:46 +00:00
{
const modOrder = this . vfs . readFile ( this . modOrderPath , { encoding : "utf8" } ) ;
2023-11-16 21:42:06 +00:00
try
2023-03-03 15:23:46 +00:00
{
2024-02-02 15:00:12 -05:00
const modOrderArray = this . jsonUtil . deserialize < any > ( modOrder , this . modOrderPath ) . order ;
for ( const [ index , mod ] of modOrderArray . entries ( ) )
{
this . order [ mod ] = index ;
}
2023-03-03 15:23:46 +00:00
}
2023-11-16 21:42:06 +00:00
catch ( error )
2023-03-03 15:23:46 +00:00
{
this . logger . error ( this . localisationService . getText ( "modloader-mod_order_error" ) ) ;
}
}
// Validate and remove broken mods from mod list
2023-11-06 14:51:31 +00:00
const validMods = this . getValidMods ( mods ) ;
2023-03-03 15:23:46 +00:00
2023-11-06 14:51:31 +00:00
const modPackageData = this . getModsPackageData ( validMods ) ;
2023-03-03 15:23:46 +00:00
this . checkForDuplicateMods ( modPackageData ) ;
2023-11-06 14:51:31 +00:00
// Used to check all errors before stopping the load execution
let errorsFound = false ;
for ( const [ modFolderName , modToValidate ] of modPackageData )
2023-03-03 15:23:46 +00:00
{
2023-11-06 14:51:31 +00:00
if ( this . shouldSkipMod ( modToValidate ) )
{
// skip error checking and dependency install for mods already marked as skipped.
continue ;
}
2023-03-03 15:23:46 +00:00
2023-10-10 11:03:20 +00:00
// if the mod has library dependencies check if these dependencies are bundled in the server, if not install them
2023-11-16 21:42:06 +00:00
if (
modToValidate . dependencies && Object . keys ( modToValidate . dependencies ) . length > 0
&& ! this . vfs . exists ( ` ${ this . basepath } ${ modFolderName } /node_modules ` )
)
2023-10-10 11:03:20 +00:00
{
this . autoInstallDependencies ( ` ${ this . basepath } ${ modFolderName } ` , modToValidate ) ;
}
2023-03-03 15:23:46 +00:00
// Returns if any mod dependency is not satisfied
if ( ! this . areModDependenciesFulfilled ( modToValidate , modPackageData ) )
{
errorsFound = true ;
}
// Returns if at least two incompatible mods are found
if ( ! this . isModCompatible ( modToValidate , modPackageData ) )
{
errorsFound = true ;
}
// Returns if mod isnt compatible with this verison of aki
if ( ! this . isModCombatibleWithAki ( modToValidate ) )
{
errorsFound = true ;
}
}
if ( errorsFound )
{
this . logger . error ( this . localisationService . getText ( "modloader-no_mods_loaded" ) ) ;
return ;
}
// sort mod order
const missingFromOrderJSON = { } ;
2023-11-06 14:51:31 +00:00
validMods . sort ( ( prev , next ) = > this . sortMods ( prev , next , missingFromOrderJSON ) ) ;
2023-03-03 15:23:46 +00:00
// log the missing mods from order.json
2024-02-02 15:00:12 -05:00
for ( const missingMod of Object . keys ( missingFromOrderJSON ) )
{
this . logger . debug ( this . localisationService . getText ( "modloader-mod_order_missing_from_json" , missingMod ) ) ;
}
2023-03-03 15:23:46 +00:00
// add mods
2023-11-06 14:51:31 +00:00
for ( const mod of validMods )
2023-03-03 15:23:46 +00:00
{
2023-11-06 14:51:31 +00:00
const pkg = modPackageData . get ( mod ) ;
if ( this . shouldSkipMod ( pkg ) )
{
this . logger . warning ( this . localisationService . getText ( "modloader-skipped_mod" , { mod : mod } ) ) ;
continue ;
}
await this . addModAsync ( mod , pkg ) ;
2023-03-03 15:23:46 +00:00
}
2023-10-18 14:44:29 +00:00
this . modLoadOrder . setModList ( this . imported ) ;
2023-03-03 15:23:46 +00:00
}
2023-08-02 08:50:04 +01:00
protected sortMods ( prev : string , next : string , missingFromOrderJSON : Record < string , boolean > ) : number
{
const previndex = this . order [ prev ] ;
const nextindex = this . order [ next ] ;
2023-11-16 21:42:06 +00:00
2023-08-02 08:50:04 +01:00
// mod is not on the list, move the mod to last
2023-11-16 21:42:06 +00:00
if ( previndex === undefined )
2023-08-02 08:50:04 +01:00
{
missingFromOrderJSON [ prev ] = true ;
return 1 ;
}
2024-02-02 15:00:12 -05:00
if ( nextindex === undefined )
2023-08-02 08:50:04 +01:00
{
missingFromOrderJSON [ next ] = true ;
return - 1 ;
}
return previndex - nextindex ;
}
2023-03-03 15:23:46 +00:00
/ * *
2023-07-22 11:46:38 +01:00
* Check for duplicate mods loaded , show error if any
2023-11-06 14:51:31 +00:00
* @param modPackageData map of mod package . json data
2023-03-03 15:23:46 +00:00
* /
2023-11-06 14:51:31 +00:00
protected checkForDuplicateMods ( modPackageData : Map < string , IPackageJsonData > ) : void
2023-03-03 15:23:46 +00:00
{
2023-11-06 14:51:31 +00:00
const grouppedMods : Map < string , IPackageJsonData [ ] > = new Map ( ) ;
for ( const mod of modPackageData . values ( ) )
2023-03-03 15:23:46 +00:00
{
2023-11-06 14:51:31 +00:00
const name = ` ${ mod . author } - ${ mod . name } ` ;
grouppedMods . set ( name , [ . . . ( grouppedMods . get ( name ) ? ? [ ] ) , mod ] ) ;
// if there's more than one entry for a given mod it means there's at least 2 mods with the same author and name trying to load.
if ( grouppedMods . get ( name ) . length > 1 && ! this . skippedMods . has ( name ) )
{
this . skippedMods . add ( name ) ;
}
2023-03-03 15:23:46 +00:00
}
2023-11-06 14:51:31 +00:00
// at this point this.skippedMods only contains mods that are duplicated, so we can just go through every single entry and log it
for ( const modName of this . skippedMods )
2023-03-03 15:23:46 +00:00
{
2023-11-06 14:51:31 +00:00
this . logger . error ( this . localisationService . getText ( "modloader-x_duplicates_found" , modName ) ) ;
2023-03-03 15:23:46 +00:00
}
}
/ * *
2023-11-06 14:51:31 +00:00
* Returns an array of valid mods .
2023-11-16 21:42:06 +00:00
*
2023-03-03 15:23:46 +00:00
* @param mods mods to validate
2023-11-06 14:51:31 +00:00
* @returns array of mod folder names
2023-03-03 15:23:46 +00:00
* /
2023-11-06 14:51:31 +00:00
protected getValidMods ( mods : string [ ] ) : string [ ]
2023-03-03 15:23:46 +00:00
{
2023-11-06 14:51:31 +00:00
const validMods : string [ ] = [ ] ;
2023-03-03 15:23:46 +00:00
for ( const mod of mods )
{
2023-11-06 14:51:31 +00:00
if ( this . validMod ( mod ) )
2023-03-03 15:23:46 +00:00
{
2023-11-06 14:51:31 +00:00
validMods . push ( mod ) ;
2023-03-03 15:23:46 +00:00
}
}
2023-11-06 14:51:31 +00:00
return validMods ;
2023-03-03 15:23:46 +00:00
}
/ * *
* Get packageJson data for mods
* @param mods mods to get packageJson for
2023-11-06 14:51:31 +00:00
* @returns map < modFolderName - package.json >
2023-03-03 15:23:46 +00:00
* /
2023-11-06 14:51:31 +00:00
protected getModsPackageData ( mods : string [ ] ) : Map < string , IPackageJsonData >
2023-03-03 15:23:46 +00:00
{
2023-11-06 14:51:31 +00:00
const loadedMods = new Map < string , IPackageJsonData > ( ) ;
2023-03-03 15:23:46 +00:00
for ( const mod of mods )
{
2023-11-06 14:51:31 +00:00
loadedMods . set ( mod , this . jsonUtil . deserialize ( this . vfs . readFile ( ` ${ this . getModPath ( mod ) } /package.json ` ) ) ) ;
2023-03-03 15:23:46 +00:00
}
return loadedMods ;
}
2023-10-20 12:23:19 +01:00
/ * *
* Is the passed in mod compatible with the running server version
* @param mod Mod to check compatibiltiy with AKI
* @returns True if compatible
* /
2023-03-03 15:23:46 +00:00
protected isModCombatibleWithAki ( mod : IPackageJsonData ) : boolean
{
2024-04-18 07:54:16 +00:00
const akiVersion = globalThis . G_AKIVERSION || this . akiConfig . akiVersion ;
2023-03-03 15:23:46 +00:00
const modName = ` ${ mod . author } - ${ mod . name } ` ;
// Error and prevent loading If no akiVersion property exists
if ( ! mod . akiVersion )
{
this . logger . error ( this . localisationService . getText ( "modloader-missing_akiversion_field" , modName ) ) ;
2024-03-16 19:18:25 +00:00
2023-03-03 15:23:46 +00:00
return false ;
}
// Error and prevent loading if akiVersion property is not a valid semver string
if ( ! ( semver . valid ( mod . akiVersion ) || semver . validRange ( mod . akiVersion ) ) )
{
this . logger . error ( this . localisationService . getText ( "modloader-invalid_akiversion_field" , modName ) ) ;
2024-03-16 19:18:25 +00:00
2023-03-03 15:23:46 +00:00
return false ;
}
2024-04-15 14:12:16 +01:00
// Warning and allow loading if semver is not satisfied
2023-03-03 15:23:46 +00:00
if ( ! semver . satisfies ( akiVersion , mod . akiVersion ) )
{
2024-04-15 14:12:16 +01:00
this . logger . warning ( this . localisationService . getText ( "modloader-outdated_akiversion_field" , modName ) ) ;
2024-03-16 19:18:25 +00:00
2024-04-15 14:12:16 +01:00
return true ;
2023-03-03 15:23:46 +00:00
}
return true ;
}
2023-10-20 12:23:19 +01:00
/ * *
* Execute each mod found in this . imported
* @returns void promise
* /
2024-03-24 17:52:38 +00:00
protected async executeModsAsync ( ) : Promise < void >
2023-03-03 15:23:46 +00:00
{
2023-10-20 12:23:19 +01:00
// Sort mods load order
2023-03-03 15:23:46 +00:00
const source = this . sortModsLoadOrder ( ) ;
2023-10-20 12:23:19 +01:00
// Import mod classes
2023-03-03 15:23:46 +00:00
for ( const mod of source )
{
2023-10-20 12:23:19 +01:00
if ( ! this . imported [ mod ] . main )
{
this . logger . error ( this . localisationService . getText ( "modloader-mod_has_no_main_property" , mod ) ) ;
2023-03-03 15:23:46 +00:00
2023-10-20 12:23:19 +01:00
continue ;
}
const filepath = ` ${ this . getModPath ( mod ) } ${ this . imported [ mod ] . main } ` ;
// Import class
const modFilePath = ` ${ process . cwd ( ) } / ${ filepath } ` ;
// eslint-disable-next-line @typescript-eslint/no-var-requires
const requiredMod = require ( modFilePath ) ;
if ( ! this . modTypeCheck . isPostV3Compatible ( requiredMod . mod ) )
2023-03-03 15:23:46 +00:00
{
2023-10-20 12:23:19 +01:00
this . logger . error ( this . localisationService . getText ( "modloader-mod_incompatible" , mod ) ) ;
delete this . imported [ mod ] ;
2023-03-03 15:23:46 +00:00
2023-10-20 12:23:19 +01:00
return ;
}
2023-03-03 15:23:46 +00:00
2023-10-20 12:23:19 +01:00
// Perform async load of mod
if ( this . modTypeCheck . isPreAkiLoadAsync ( requiredMod . mod ) )
{
2023-11-16 21:42:06 +00:00
try
2023-03-03 15:23:46 +00:00
{
2024-03-24 17:52:38 +00:00
await ( requiredMod . mod as IPreAkiLoadModAsync ) . preAkiLoadAsync ( this . container ) ;
2023-10-20 12:23:19 +01:00
globalThis [ mod ] = requiredMod ;
2023-03-03 15:23:46 +00:00
}
2023-11-16 21:42:06 +00:00
catch ( err )
2023-10-10 11:03:20 +00:00
{
2023-11-16 21:42:06 +00:00
this . logger . error (
this . localisationService . getText (
"modloader-async_mod_error" ,
` ${ err ? . message ? ? "" } \ n ${ err . stack ? ? "" } ` ,
) ,
) ;
2023-10-10 11:03:20 +00:00
}
2023-10-20 12:23:19 +01:00
continue ;
}
// Perform sync load of mod
if ( this . modTypeCheck . isPreAkiLoad ( requiredMod . mod ) )
{
2024-03-24 17:52:38 +00:00
( requiredMod . mod as IPreAkiLoadMod ) . preAkiLoad ( this . container ) ;
2023-10-20 12:23:19 +01:00
globalThis [ mod ] = requiredMod ;
2023-03-03 15:23:46 +00:00
}
}
}
2023-10-20 12:23:19 +01:00
/ * *
* Read loadorder . json ( create if doesnt exist ) and return sorted list of mods
* @returns string array of sorted mod names
* /
2023-03-03 15:23:46 +00:00
public sortModsLoadOrder ( ) : string [ ]
{
// if loadorder.json exists: load it, otherwise generate load order
2024-02-02 13:54:07 -05:00
const loadOrderPath = ` ${ this . basepath } loadorder.json ` ;
2023-12-13 22:16:21 +00:00
if ( this . vfs . exists ( loadOrderPath ) )
2023-03-03 15:23:46 +00:00
{
2023-12-13 22:16:21 +00:00
return this . jsonUtil . deserialize ( this . vfs . readFile ( loadOrderPath ) , loadOrderPath ) ;
2023-03-03 15:23:46 +00:00
}
2024-02-02 15:00:12 -05:00
return this . modLoadOrder . getLoadOrder ( ) ;
2023-03-03 15:23:46 +00:00
}
2023-10-14 09:48:24 +01:00
/ * *
* Compile mod and add into class property "imported"
* @param mod Name of mod to compile / add
* /
2023-11-06 14:51:31 +00:00
protected async addModAsync ( mod : string , pkg : IPackageJsonData ) : Promise < void >
2023-03-03 15:23:46 +00:00
{
const modPath = this . getModPath ( mod ) ;
const typeScriptFiles = this . vfs . getFilesOfType ( ` ${ modPath } src ` , ".ts" ) ;
if ( typeScriptFiles . length > 0 )
{
if ( globalThis . G_MODS_TRANSPILE_TS )
{
// compile ts into js if ts files exist and globalThis.G_MODS_TRANSPILE_TS is set to true
await this . modCompilerService . compileMod ( mod , modPath , typeScriptFiles ) ;
}
else
{
// rename the mod entry point to .ts if it's set to .js because G_MODS_TRANSPILE_TS is set to false
2023-11-16 21:42:06 +00:00
pkg . main = pkg . main . replace ( ".js" , ".ts" ) ;
2023-03-03 15:23:46 +00:00
}
}
2023-10-14 09:48:24 +01:00
// Purge scripts data from package object
2023-11-06 14:51:31 +00:00
pkg . scripts = { } ;
2023-10-14 09:48:24 +01:00
// Add mod to imported list
2023-11-16 21:42:06 +00:00
this . imported [ mod ] = { . . . pkg , dependencies : pkg.modDependencies } ;
this . logger . info (
this . localisationService . getText ( "modloader-loaded_mod" , {
name : pkg.name ,
version : pkg.version ,
author : pkg.author ,
} ) ,
) ;
2023-11-06 14:51:31 +00:00
}
/ * *
* Checks if a given mod should be loaded or skipped .
2023-11-16 21:42:06 +00:00
*
2023-11-06 14:51:31 +00:00
* @param pkg mod package . json data
2023-11-16 21:42:06 +00:00
* @returns
2023-11-06 14:51:31 +00:00
* /
protected shouldSkipMod ( pkg : IPackageJsonData ) : boolean
{
return this . skippedMods . has ( ` ${ pkg . author } - ${ pkg . name } ` ) ;
2023-03-03 15:23:46 +00:00
}
2023-10-10 11:03:20 +00:00
protected autoInstallDependencies ( modPath : string , pkg : IPackageJsonData ) : void
{
2024-03-18 00:06:08 +00:00
const dependenciesToInstall = new Map < string , string > ( ) ;
2023-10-10 11:03:20 +00:00
2024-03-17 23:19:13 +00:00
for ( const [ depName , depVersion ] of Object . entries ( pkg . dependencies ) )
2023-10-10 11:03:20 +00:00
{
// currently not checking for version mismatches, but we could check it, just don't know what we would do afterwards, some options would be:
// 1 - throw an error
// 2 - use the server's version (which is what's currently happening by not checking the version)
// 3 - use the mod's version (don't know the reprecursions this would have, or if it would even work)
2024-03-17 23:19:13 +00:00
// if a mod's dependency does not exist in the server's dependencies we can add it to the list of dependencies to install.
if ( ! this . serverDependencies [ depName ] )
2023-10-10 11:03:20 +00:00
{
2024-03-18 00:06:08 +00:00
dependenciesToInstall . set ( depName , depVersion ) ;
2023-10-10 11:03:20 +00:00
}
}
2023-10-20 12:23:19 +01:00
// If the mod has no extra dependencies return as there's nothing that needs to be done.
2024-03-18 00:06:08 +00:00
if ( dependenciesToInstall . size === 0 )
2023-10-10 11:03:20 +00:00
{
return ;
}
2023-10-20 12:23:19 +01:00
// If this feature flag is set to false, we warn the user he has a mod that requires extra dependencies and might not work, point them in the right direction on how to enable this feature.
2023-10-10 11:03:20 +00:00
if ( ! this . akiConfig . features . autoInstallModDependencies )
{
2023-11-16 21:42:06 +00:00
this . logger . warning (
this . localisationService . getText ( "modloader-installing_external_dependencies_disabled" , {
name : pkg.name ,
author : pkg.author ,
configPath : path.join (
globalThis . G_RELEASE_CONFIGURATION ? "Aki_Data/Server/configs" : "assets/configs" ,
"core.json" ,
) ,
configOption : "autoInstallModDependencies" ,
} ) ,
) ;
2023-10-10 11:03:20 +00:00
2023-11-06 14:51:31 +00:00
this . skippedMods . add ( ` ${ pkg . author } - ${ pkg . name } ` ) ;
2023-10-10 11:03:20 +00:00
return ;
}
2023-10-20 12:23:19 +01:00
// Temporarily rename package.json because otherwise npm, pnpm and any other package manager will forcefully download all packages in dependencies without any way of disabling this behavior
2023-10-10 11:03:20 +00:00
this . vfs . rename ( ` ${ modPath } /package.json ` , ` ${ modPath } /package.json.bak ` ) ;
this . vfs . writeFile ( ` ${ modPath } /package.json ` , "{}" ) ;
2023-11-16 21:42:06 +00:00
this . logger . info (
this . localisationService . getText ( "modloader-installing_external_dependencies" , {
name : pkg.name ,
author : pkg.author ,
} ) ,
) ;
const pnpmPath = path . join (
process . cwd ( ) ,
globalThis . G_RELEASE_CONFIGURATION ? "Aki_Data/Server/@pnpm/exe" : "node_modules/@pnpm/exe" ,
os . platform ( ) === "win32" ? "pnpm.exe" : "pnpm" ,
) ;
2024-03-18 00:06:08 +00:00
2023-10-20 12:23:19 +01:00
let command = ` ${ pnpmPath } install ` ;
2024-03-18 00:06:08 +00:00
for ( const [ depName , depVersion ] of dependenciesToInstall )
{
command += ` ${ depName } @ ${ depVersion } ` ;
}
2023-10-10 11:03:20 +00:00
execSync ( command , { cwd : modPath } ) ;
2023-10-20 12:23:19 +01:00
// Delete the new blank package.json then rename the backup back to the original name
2023-10-10 11:03:20 +00:00
this . vfs . removeFile ( ` ${ modPath } /package.json ` ) ;
this . vfs . rename ( ` ${ modPath } /package.json.bak ` , ` ${ modPath } /package.json ` ) ;
}
2023-11-06 14:51:31 +00:00
protected areModDependenciesFulfilled ( pkg : IPackageJsonData , loadedMods : Map < string , IPackageJsonData > ) : boolean
2023-03-03 15:23:46 +00:00
{
if ( ! pkg . modDependencies )
{
return true ;
}
const modName = ` ${ pkg . author } - ${ pkg . name } ` ;
2023-11-16 21:42:06 +00:00
for ( const [ modDependency , requiredVersion ] of Object . entries ( pkg . modDependencies ) )
2023-03-03 15:23:46 +00:00
{
// Raise dependency version incompatible if the dependency is not found in the mod list
2023-11-06 14:51:31 +00:00
if ( ! loadedMods . has ( modDependency ) )
2023-03-03 15:23:46 +00:00
{
2023-11-16 21:42:06 +00:00
this . logger . error (
this . localisationService . getText ( "modloader-missing_dependency" , {
mod : modName ,
modDependency : modDependency ,
} ) ,
) ;
2023-03-03 15:23:46 +00:00
return false ;
}
2023-11-06 14:51:31 +00:00
if ( ! semver . satisfies ( loadedMods . get ( modDependency ) . version , requiredVersion ) )
2023-03-03 15:23:46 +00:00
{
2023-11-16 21:42:06 +00:00
this . logger . error (
this . localisationService . getText ( "modloader-outdated_dependency" , {
mod : modName ,
modDependency : modDependency ,
currentVersion : loadedMods.get ( modDependency ) . version ,
requiredVersion : requiredVersion ,
} ) ,
) ;
2023-03-03 15:23:46 +00:00
return false ;
}
}
return true ;
}
2023-11-06 14:51:31 +00:00
protected isModCompatible ( mod : IPackageJsonData , loadedMods : Map < string , IPackageJsonData > ) : boolean
2023-03-03 15:23:46 +00:00
{
const incompatbileModsList = mod . incompatibilities ;
if ( ! incompatbileModsList )
{
return true ;
}
for ( const incompatibleModName of incompatbileModsList )
{
// Raise dependency version incompatible if any incompatible mod is found
2023-11-06 14:51:31 +00:00
if ( loadedMods . has ( incompatibleModName ) )
2023-03-03 15:23:46 +00:00
{
2023-11-16 21:42:06 +00:00
this . logger . error (
this . localisationService . getText ( "modloader-incompatible_mod_found" , {
author : mod.author ,
modName : mod.name ,
incompatibleModName : incompatibleModName ,
} ) ,
) ;
2023-03-03 15:23:46 +00:00
return false ;
}
}
return true ;
}
/ * *
* Validate a mod passes a number of checks
* @param modName name of mod in /mods/ to validate
* @returns true if valid
* /
protected validMod ( modName : string ) : boolean
{
const modPath = this . getModPath ( modName ) ;
const modIsCalledBepinEx = modName . toLowerCase ( ) === "bepinex" ;
2023-10-10 11:03:20 +00:00
const modIsCalledUser = modName . toLowerCase ( ) === "user" ;
const modIsCalledSrc = modName . toLowerCase ( ) === "src" ;
const modIsCalledDb = modName . toLowerCase ( ) === "db" ;
2023-03-03 15:23:46 +00:00
const hasBepinExFolderStructure = this . vfs . exists ( ` ${ modPath } /plugins ` ) ;
2023-11-16 21:42:06 +00:00
const containsDll = this . vfs . getFiles ( ` ${ modPath } ` ) . find ( ( x ) = > x . includes ( ".dll" ) ) ;
2023-10-10 11:03:20 +00:00
if ( modIsCalledSrc || modIsCalledDb || modIsCalledUser )
{
this . logger . error ( this . localisationService . getText ( "modloader-not_correct_mod_folder" , modName ) ) ;
return false ;
}
2023-03-03 15:23:46 +00:00
if ( modIsCalledBepinEx || hasBepinExFolderStructure || containsDll )
{
this . logger . error ( this . localisationService . getText ( "modloader-is_client_mod" , modName ) ) ;
return false ;
}
2023-10-10 11:03:20 +00:00
// Check if config exists
2023-12-13 22:16:21 +00:00
const modPackagePath = ` ${ modPath } /package.json ` ;
if ( ! this . vfs . exists ( modPackagePath ) )
2023-03-03 15:23:46 +00:00
{
this . logger . error ( this . localisationService . getText ( "modloader-missing_package_json" , modName ) ) ;
return false ;
}
2023-10-10 11:03:20 +00:00
// Validate mod
2023-12-13 22:16:21 +00:00
const config = this . jsonUtil . deserialize < IPackageJsonData > ( this . vfs . readFile ( modPackagePath ) , modPackagePath ) ;
2023-03-03 15:23:46 +00:00
const checks = [ "name" , "author" , "version" , "license" ] ;
let issue = false ;
for ( const check of checks )
{
if ( ! ( check in config ) )
{
2023-11-16 21:42:06 +00:00
this . logger . error (
this . localisationService . getText ( "modloader-missing_package_json_property" , {
modName : modName ,
prop : check ,
} ) ,
) ;
2023-03-03 15:23:46 +00:00
issue = true ;
}
}
if ( ! semver . valid ( config . version ) )
{
this . logger . error ( this . localisationService . getText ( "modloader-invalid_version_property" , modName ) ) ;
issue = true ;
}
if ( "main" in config )
{
2023-11-16 21:42:06 +00:00
if ( config . main . split ( "." ) . pop ( ) !== "js" )
{ // expects js file as entry
2023-03-03 15:23:46 +00:00
this . logger . error ( this . localisationService . getText ( "modloader-main_property_not_js" , modName ) ) ;
issue = true ;
}
if ( ! this . vfs . exists ( ` ${ modPath } / ${ config . main } ` ) )
{
// If TS file exists with same name, dont perform check as we'll generate JS from TS file
const tsFileName = config . main . replace ( ".js" , ".ts" ) ;
const tsFileExists = this . vfs . exists ( ` ${ modPath } / ${ tsFileName } ` ) ;
if ( ! tsFileExists )
{
2023-11-16 21:42:06 +00:00
this . logger . error (
this . localisationService . getText ( "modloader-main_property_points_to_nothing" , modName ) ,
) ;
2023-03-03 15:23:46 +00:00
issue = true ;
}
}
}
if ( config . incompatibilities && ! Array . isArray ( config . incompatibilities ) )
{
2023-11-16 21:42:06 +00:00
this . logger . error (
this . localisationService . getText ( "modloader-incompatibilities_not_string_array" , modName ) ,
) ;
2023-03-03 15:23:46 +00:00
issue = true ;
}
return ! issue ;
}
public getContainer ( ) : DependencyContainer
{
2024-03-24 17:52:38 +00:00
if ( this . container )
2023-03-03 15:23:46 +00:00
{
2024-03-24 17:52:38 +00:00
return this . container ;
2023-03-03 15:23:46 +00:00
}
2024-02-02 15:00:12 -05:00
throw new Error ( this . localisationService . getText ( "modloader-dependency_container_not_initalized" ) ) ;
2023-03-03 15:23:46 +00:00
}
2023-10-18 14:44:29 +00:00
}