diff --git a/readme.md b/README.md similarity index 52% rename from readme.md rename to README.md index e2212b4..e2122ea 100644 --- a/readme.md +++ b/README.md @@ -1,10 +1,11 @@ -# Mod examples for 3.7.0 +# Mod examples for v3.7.0 A collection of example mods that perform typical actions in SPT # Setup -Download and put each folder in user/mods -# Mod upgrade guide +Dive into a specific mod folder and follow the instructions in the `README.md` file. + +# Mod Upgrade Guide + Read [Here](https://hub.sp-tarkov.com/doc/entry/51-modding-in-2-4-0/) - diff --git a/TypeScript/10ScopesAndTypes/.buildignore b/TypeScript/10ScopesAndTypes/.buildignore new file mode 100644 index 0000000..2cbde65 --- /dev/null +++ b/TypeScript/10ScopesAndTypes/.buildignore @@ -0,0 +1,20 @@ +/.buildignore +/.DS_Store +/.editorconfig +/.eslintignore +/.eslintrc.json +/.git +/.github +/.gitignore +/.gitlab +/.nvmrc +/.prettierrc +/.vscode +/build.mjs +/dist +/images +/mod.code-workspace +/node_modules +/package-lock.json +/tsconfig.json +/types diff --git a/TypeScript/10ScopesAndTypes/.eslintrc.json b/TypeScript/10ScopesAndTypes/.eslintrc.json index c505160..071a313 100644 --- a/TypeScript/10ScopesAndTypes/.eslintrc.json +++ b/TypeScript/10ScopesAndTypes/.eslintrc.json @@ -1,75 +1,98 @@ { - "root": true, - "parser": "@typescript-eslint/parser", - "plugins": [ - "@typescript-eslint" - ], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended" - ], - "rules": { - "@typescript-eslint/no-explicit-any": 0, - "@typescript-eslint/no-unused-vars": 1, - "@typescript-eslint/no-empty-interface": 0, - "@typescript-eslint/no-namespace": 0, - "@typescript-eslint/comma-dangle": 1, - "@typescript-eslint/func-call-spacing": 2, - "@typescript-eslint/quotes": 1, - "@typescript-eslint/brace-style": [ - "warn", - "allman" + "root": true, + "parser": "@typescript-eslint/parser", + "plugins": [ + "@typescript-eslint" ], - "@typescript-eslint/naming-convention": [ - "warn", - { - "selector": "default", - "format": ["camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "typeLike", - "format": ["PascalCase"] - }, - { - "selector": "objectLiteralProperty", - "format": ["PascalCase", "camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "typeProperty", - "format": ["PascalCase", "camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "enumMember", - "format": ["UPPER_CASE"] - } + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" ], - "@typescript-eslint/indent": [ - "warn", - 4 - ], - "@typescript-eslint/no-unused-expressions": [ - "warn", - { - "allowShortCircuit": false, - "allowTernary": false - } - ], - "@typescript-eslint/keyword-spacing": [ - "warn", - { - "before": true, - "after": true - } - ], - "@typescript-eslint/explicit-module-boundary-types": [ - "warn", - { - "allowArgumentsExplicitlyTypedAsAny": true - } + "rules": { + "@typescript-eslint/no-explicit-any": 0, + "@typescript-eslint/no-unused-vars": 1, + "@typescript-eslint/no-empty-interface": 0, + "@typescript-eslint/no-namespace": 0, + "@typescript-eslint/comma-dangle": 1, + "@typescript-eslint/func-call-spacing": 2, + "@typescript-eslint/quotes": 1, + "@typescript-eslint/brace-style": [ + "warn", + "allman" + ], + "@typescript-eslint/naming-convention": [ + "warn", + { + "selector": "default", + "format": [ + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "typeLike", + "format": [ + "PascalCase" + ] + }, + { + "selector": "objectLiteralProperty", + "format": [ + "PascalCase", + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "typeProperty", + "format": [ + "PascalCase", + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "enumMember", + "format": [ + "UPPER_CASE" + ] + } + ], + "@typescript-eslint/indent": [ + "warn", + 4 + ], + "@typescript-eslint/no-unused-expressions": [ + "warn", + { + "allowShortCircuit": false, + "allowTernary": false + } + ], + "@typescript-eslint/keyword-spacing": [ + "warn", + { + "before": true, + "after": true + } + ], + "@typescript-eslint/explicit-module-boundary-types": [ + "warn", + { + "allowArgumentsExplicitlyTypedAsAny": true + } + ] + }, + "overrides": [ + { + "files": [ + "*.mjs", + "*.ts" + ], + "env": { + "node": true + } + } ] - } } \ No newline at end of file diff --git a/TypeScript/10ScopesAndTypes/README.md b/TypeScript/10ScopesAndTypes/README.md index 434d063..8cba28e 100644 --- a/TypeScript/10ScopesAndTypes/README.md +++ b/TypeScript/10ScopesAndTypes/README.md @@ -1,64 +1,66 @@ +# Welcome to the SPT-AKI Modding Project -This project was created to automate most parts of building and setting up an environment. +This project is designed to streamline the initial setup process for building and creating mods in the SPT-AKI environment. Follow this guide to set up your environment efficiently. -## **NodeJS:** +## **Table of Contents** +- [NodeJS Setup](#nodejs-setup) +- [IDE Setup](#ide-setup) +- [Workspace Configuration](#workspace-configuration) +- [Environment Setup](#environment-setup) +- [Essential Concepts](#essential-concepts) +- [Coding Guidelines](#coding-guidelines) +- [Distribution Guidelines](#distribution-guidelines) -The first step would be to install nodejs on your pc, the version you NEED is **16.17.1** +## **NodeJS Setup** -That version is the one that has been used to test the mod templates and build scripts. +Before you begin, ensure to install NodeJS version `v16.17.1`, which has been tested thoroughly with our mod templates and build scripts. Download it from the [official NodeJS website](https://nodejs.org/). -It can be downloaded from here: https://nodejs.org/dist/v16.17.1/node-v16.17.1-x64.msi +After installation, it's advised to reboot your system. -A system reboot may be needed after install. +## **IDE Setup** -## **IDE:** +For this project, you can work with either [VSCodium](https://vscodium.com/) or [VSCode](https://code.visualstudio.com/). However, we strongly recommend using VSCode, as all development and testing have been carried out using this IDE, ensuring a smoother experience and compatibility with the project setups. Either way, we have a prepared a workspace file to assist you in setting up your environment. -The second step is having an IDE ready. We've setup a VSCodium workspace file to help with this. +## **Workspace Configuration** -You CAN use Visual Studio Code if you so desire, just keep in mind that our devs tests on the mod files was done using VSCode. +With NodeJS and your chosen IDE ready, initiate the `mod.code-workspace` file using your IDE: -You can get VSCode here: https://code.visualstudio.com/ +> File -> Open Workspace from File... -## **Workspace:** +Upon project loading, consider installing recommended plugins like the ESLint plugin. -Once you have NodeJS and VSCode/VSCodium ready, open the mod.code-workspace file with VSCode (File->Open Workspace from File...). +## **Environment Setup** -Once the project loads you may be recommended to install the ESLint plugin. This is HIGHLY recommended. +An automated task is available to configure your environment for Typescript utilization: -## **Environment Setup:** +> Terminal -> Run Task... -> Show All Tasks... -> npm: install -There is a task that will automatically setup your environment to use typescript. +Note: Preserve the `node_modules` folder as it contains necessary dependencies for Typescript and other functionalities. -To run it, you just need to go to: +## **Essential Concepts** -> Terminal->Run Task...->Show All Tasks...->npm: install +Prioritize understanding Dependency Injection and Inversion of Control, the architectural principles SPT-AKI adopts. Comprehensive guidelines will be available on the hub upon release. -After running this task, your environment will be ready to start coding. +Some resources to get you started: + - [A quick intro to Dependency Injection](https://www.freecodecamp.org/news/a-quick-intro-to-dependency-injection-what-it-is-and-when-to-use-it-7578c84fa88f/) + - [Understanding Inversion of Control (IoC) Principle](https://medium.com/@amitkma/understanding-inversion-of-control-ioc-principle-163b1dc97454) -DO NOT remove the node_modules folder, this is an auto generated directory that has the required dependencies to be able to use typescript and more. +## **Coding Guidelines** -## **IMPORTANT:** +Focus your mod development around the `mod.ts` file. In the `package.json` file, only alter these properties: `"name"`, `"version"`, `"license"`, `"author"`, and `"akiVersion"`. -Before starting to work on your mod, we suggest you read about Dependency Injection and Inversion of Control as this is the adopted architecture SPT-AKI has adopted. +New to Typescript? Find comprehensive documentation on the [official website](https://www.typescriptlang.org/docs/). -It will be difficult to understand some of the problems you may be having if you dont understand the basics of it. +## **Distribution Guidelines** -A guide explaining all the essentials will be available on the hub on release for you to read about. +Automated tasks are set up to bundle all necessary files for your mod to function in SPT-AKI: -## **Coding:** +> Terminal -> Run Task... -> Show All Tasks... -> npm: build -All your work should be centered around the mod.ts file as an entry point. -You can ONLY change the following properties from the package.json file: `"name"`, `"version"`, `"license"`: `"MIT"`, `"author"`, `"akiVersion"`. +The ZIP output, located in the `dist` directory, contains all required files. Ensure all files are included and modify the `.buildignore` file as needed. This ZIP file is your uploadable asset for the hub. -If you have never used typescript before, you can read about it here: https://www.typescriptlang.org/docs/ +## **Conclusion** -## **Distributing your mod:** +With this setup, you're ready to begin modding with SPT-AKI. If you run into any trouble be sure to check out the [modding documentation on the hub](https://hub.sp-tarkov.com/doc/lexicon/66-modding/). If you really get stuck feel free to join us in the [#mods-development](https://discord.com/channels/875684761291599922/875803116409323562) official Discord channel. -The project has been set up with an automatic task that will copy and zip ALL required files for your mod to work on SPT-AKI. -To run this task you just need to go to: - -> Terminal->Run Task...->Show All Tasks...->npm: build:zip - -The output will be a mod.zip file that will appear on the root of the project. - -Always verify that all files were included into the zip file. \ No newline at end of file +Build something awesome! diff --git a/TypeScript/10ScopesAndTypes/build.mjs b/TypeScript/10ScopesAndTypes/build.mjs new file mode 100644 index 0000000..d00117c --- /dev/null +++ b/TypeScript/10ScopesAndTypes/build.mjs @@ -0,0 +1,383 @@ +#!/usr/bin/env node + +/** + * Build Script + * + * This script automates the build process for server-side SPT mod projects, facilitating the creation of distributable + * mod packages. It performs a series of operations as outlined below: + * - Loads the .buildignore file, which is used to list files that should be ignored during the build process. + * - Loads the package.json to get project details so a descriptive name can be created for the mod package. + * - Creates a distribution directory and a temporary working directory. + * - Copies files to the temporary directory while respecting the .buildignore rules. + * - Creates a zip archive of the project files. + * - Moves the zip file to the root of the distribution directory. + * - Cleans up the temporary directory. + * + * It's typical that this script be customized to suit the needs of each project. For example, the script can be updated + * to perform additional operations, such as moving the mod package to a specific location or uploading it to a server. + * This script is intended to be a starting point for developers to build upon. + * + * Usage: + * - Run this script using npm: `npm run build` + * - Use `npm run buildinfo` for detailed logging. + * + * Note: + * - Ensure that all necessary Node.js modules are installed before running the script: `npm install` + * - The script reads configurations from the `package.json` and `.buildignore` files; ensure they are correctly set up. + * + * @author Refringe + * @version v1.0.0 + */ + +import fs from "fs-extra"; +import os from "os"; +import path from "path"; +import { fileURLToPath } from "url"; +import { dirname } from "path"; +import ignore from "ignore"; +import archiver from "archiver"; +import winston from "winston"; + +// Get the command line arguments to determine whether to use verbose logging. +const args = process.argv.slice(2); +const verbose = args.includes("--verbose") || args.includes("-v"); + +// Configure the Winston logger to use colours. +const logColors = { + error: "red", + warn: "yellow", + info: "grey", + success: "green", +}; +winston.addColors(logColors); + +// Create a logger instance to log build progress. Configure the logger levels to allow for different levels of logging +// based on the verbosity flag, and set the console transport to log messages of the appropriate level. +const logger = winston.createLogger({ + levels: { + error: 0, + warn: 1, + success: 2, + info: 3, + }, + format: winston.format.combine( + winston.format.colorize(), + winston.format.printf(info => { + return `${info.level}: ${info.message}`; + }) + ), + transports: [ + new winston.transports.Console({ + level: verbose ? "info" : "success", + }), + ], +}); + +/** + * The main function orchestrates the build process for creating a distributable mod package. It leverages a series of + * helper functions to perform various tasks such as loading configuration files, setting up directories, copying files + * according to `.buildignore` rules, and creating a ZIP archive of the project files. + * + * Utilizes the Winston logger to provide information on the build status at different stages of the process. + * + * @returns {void} + */ +async function main() { + // Get the current directory where the script is being executed + const currentDir = getCurrentDirectory(); + + // Defining at this scope because we need to use it in the finally block. + let projectDir; + + try { + // Load the .buildignore file to set up an ignore handler for the build process. + const buildIgnorePatterns = await loadBuildIgnoreFile(currentDir); + + // Load the package.json file to get project details. + const packageJson = await loadPackageJson(currentDir); + + // Create a descriptive name for the mod package. + const projectName = createProjectName(packageJson); + logger.log("success", `Project name created: ${projectName}`); + + // Remove the old distribution directory and create a fresh one. + const distDir = await removeOldDistDirectory(currentDir); + logger.log("info", "Distribution directory successfully cleaned."); + + // Create a temporary working directory to perform the build operations. + projectDir = await createTemporaryDirectoryWithProjectName(projectName); + logger.log("success", "Temporary working directory successfully created."); + logger.log("info", projectDir); + + // Copy files to the temporary directory while respecting the .buildignore rules. + logger.log("info", "Beginning copy operation using .buildignore file..."); + await copyFiles(currentDir, projectDir, buildIgnorePatterns); + logger.log("success", "Files successfully copied to temporary directory."); + + // Create a zip archive of the project files. + logger.log("info", "Beginning folder compression..."); + const zipFilePath = path.join(path.dirname(projectDir), `${projectName}.zip`); + await createZipFile(projectDir, zipFilePath, "user/mods/" + projectName); + logger.log("success", "Archive successfully created."); + logger.log("info", zipFilePath); + + // Move the zip file inside of the project directory, within the temporary working directory. + const zipFileInProjectDir = path.join(projectDir, `${projectName}.zip`); + await fs.move(zipFilePath, zipFileInProjectDir); + logger.log("success", "Archive successfully moved."); + logger.log("info", zipFileInProjectDir); + + // Move the temporary directory into the distribution directory. + await fs.move(projectDir, distDir); + logger.log("success", "Temporary directory successfully moved into project distribution directory."); + + // Log the success message. Write out the path to the mod package. + logger.log("success", "------------------------------------"); + logger.log("success", "Build script completed successfully!"); + logger.log("success", "Your mod package has been created in the 'dist' directory:"); + logger.log("success", `/${path.relative(process.cwd(), path.join(distDir, `${projectName}.zip`))}`); + logger.log("success", "------------------------------------"); + if (!verbose) { + logger.log("success", "To see a detailed build log, use `npm run buildinfo`."); + logger.log("success", "------------------------------------"); + } + } catch (err) { + // If any of the file operations fail, log the error. + logger.log("error", "An error occurred: " + err); + } finally { + // Clean up the temporary directory, even if the build fails. + if (projectDir) { + try { + await fs.promises.rm(projectDir, { force: true, recursive: true }); + logger.log("info", "Cleaned temporary directory."); + } catch (err) { + logger.log("error", "Failed to clean temporary directory: " + err); + } + } + } +} + +/** + * Retrieves the current working directory where the script is being executed. This directory is used as a reference + * point for various file operations throughout the build process, ensuring that paths are resolved correctly regardless + * of the location from which the script is invoked. + * + * @returns {string} The absolute path of the current working directory. + */ +function getCurrentDirectory() { + return dirname(fileURLToPath(import.meta.url)); +} + +/** + * Loads the `.buildignore` file and sets up an ignore handler using the `ignore` module. The `.buildignore` file + * contains a list of patterns describing files and directories that should be ignored during the build process. The + * ignore handler created by this method is used to filter files and directories when copying them to the temporary + * directory, ensuring that only necessary files are included in the final mod package. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to an ignore handler. + */ +async function loadBuildIgnoreFile(currentDir) { + const buildIgnorePath = path.join(currentDir, ".buildignore"); + + try { + // Attempt to read the contents of the .buildignore file asynchronously. + const fileContent = await fs.promises.readFile(buildIgnorePath, "utf-8"); + + // Return a new ignore instance and add the rules from the .buildignore file (split by newlines). + return ignore().add(fileContent.split("\n")); + } catch (err) { + logger.log("warn", "Failed to read .buildignore file. No files or directories will be ignored."); + + // Return an empty ignore instance, ensuring the build process can continue. + return ignore(); + } +} + +/** + * Loads the `package.json` file and returns its content as a JSON object. The `package.json` file contains important + * project details such as the name and version, which are used in later stages of the build process to create a + * descriptive name for the mod package. The method reads the file from the current working directory, ensuring that it + * accurately reflects the current state of the project. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to a JSON object containing the contents of the `package.json`. + */ +async function loadPackageJson(currentDir) { + const packageJsonPath = path.join(currentDir, "package.json"); + + // Read the contents of the package.json file asynchronously as a UTF-8 string. + const packageJsonContent = await fs.promises.readFile(packageJsonPath, "utf-8"); + + return JSON.parse(packageJsonContent); +} + +/** + * Constructs a descriptive name for the mod package using details from the `package.json` file. The name is created by + * concatenating the project name, version, and a timestamp, resulting in a unique and descriptive file name for each + * build. This name is used as the base name for the temporary working directory and the final ZIP archive, helping to + * identify different versions of the mod package easily. + * + * @param {Object} packageJson - A JSON object containing the contents of the `package.json` file. + * @returns {string} A string representing the constructed project name. + */ +function createProjectName(packageJson) { + // Remove any non-alphanumeric characters from the author and name. + const author = packageJson.author.replace(/\W/g, ""); + const name = packageJson.name.replace(/\W/g, ""); + const version = packageJson.version; + + // Ensure the name is lowercase, as per the package.json specification. + return `${author}-${name}-${version}`.toLowerCase(); +} + +/** + * Defines the location of the distribution directory where the final mod package will be stored and deletes any + * existing distribution directory to ensure a clean slate for the build process. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to the absolute path to the distribution directory. + */ +async function removeOldDistDirectory(projectDir) { + const distPath = path.join(projectDir, "dist"); + await fs.remove(distPath); + return distPath; +} + +/** + * Creates a temporary working directory using the project name. This directory serves as a staging area where project + * files are gathered before being archived into the final mod package. The method constructs a unique directory path + * by appending the project name to a base temporary directory path, ensuring that each build has its own isolated + * working space. This approach facilitates clean and organized build processes, avoiding potential conflicts with other + * builds. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @param {string} projectName - The constructed project name, used to create a unique path for the temporary directory. + * @returns {Promise} A promise that resolves to the absolute path of the newly created temporary directory. + */ +async function createTemporaryDirectoryWithProjectName(projectName) { + // Create a new directory in the system's temporary folder to hold the project files. + const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "spt-mod-build-")); + + // Create a subdirectory within the temporary directory using the project name for this specific build. + const projectDir = path.join(tempDir, projectName); + await fs.ensureDir(projectDir); + + return projectDir; +} + +/** + * Copies the project files to the temporary directory while respecting the rules defined in the `.buildignore` file. + * The method is recursive, iterating over all files and directories in the source directory and using the ignore + * handler to filter out files and directories that match the patterns defined in the `.buildignore` file. This ensures + * that only the necessary files are included in the final mod package, adhering to the specifications defined by the + * developer in the `.buildignore` file. + * + * The copy operations are delayed and executed in parallel to improve efficiency and reduce the build time. This is + * achieved by creating an array of copy promises and awaiting them all at the end of the function. + * + * @param {string} sourceDirectory - The absolute path of the current working directory. + * @param {string} destinationDirectory - The absolute path of the temporary directory where the files will be copied. + * @param {Ignore} ignoreHandler - The ignore handler created from the `.buildignore` file. + * @returns {Promise} A promise that resolves when all copy operations are completed successfully. + */ +async function copyFiles(srcDir, destDir, ignoreHandler) { + try { + // Read the contents of the source directory to get a list of entries (files and directories). + const entries = await fs.promises.readdir(srcDir, { withFileTypes: true }); + + // Initialize an array to hold the promises returned by recursive calls to copyFiles and copyFile operations. + const copyOperations = []; + + for (const entry of entries) { + // Define the source and destination paths for each entry. + const srcPath = path.join(srcDir, entry.name); + const destPath = path.join(destDir, entry.name); + + // Get the relative path of the source file to check against the ignore handler. + const relativePath = path.relative(process.cwd(), srcPath); + + // If the ignore handler dictates that this file should be ignored, skip to the next iteration. + if (ignoreHandler.ignores(relativePath)) { + logger.log("info", `Ignored: /${path.relative(process.cwd(), srcPath)}`); + continue; + } + + if (entry.isDirectory()) { + // If the entry is a directory, create the corresponding temporary directory and make a recursive call + // to copyFiles to handle copying the contents of the directory. + await fs.ensureDir(destPath); + copyOperations.push(copyFiles(srcPath, destPath, ignoreHandler)); + } else { + // If the entry is a file, add a copyFile operation to the copyOperations array and log the event when + // the operation is successful. + copyOperations.push( + fs.copy(srcPath, destPath).then(() => { + logger.log("info", `Copied: /${path.relative(process.cwd(), srcPath)}`); + }) + ); + } + } + + // Await all copy operations to ensure all files and directories are copied before exiting the function. + await Promise.all(copyOperations); + } catch (err) { + // Log an error message if any error occurs during the copy process. + logger.log("error", "Error copying files: " + err); + } +} + +/** + * Creates a ZIP archive of the project files located in the temporary directory. The method uses the `archiver` module + * to create a ZIP file, which includes all the files that have been copied to the temporary directory during the build + * process. The ZIP file is named using the project name, helping to identify the contents of the archive easily. + * + * @param {string} directoryPath - The absolute path of the temporary directory containing the project files. + * @param {string} projectName - The constructed project name, used to name the ZIP file. + * @returns {Promise} A promise that resolves to the absolute path of the created ZIP file. + */ +async function createZipFile(directoryToZip, zipFilePath, containerDirName) { + return new Promise((resolve, reject) => { + // Create a write stream to the specified ZIP file path. + const output = fs.createWriteStream(zipFilePath); + + // Create a new archiver instance with ZIP format and maximum compression level. + const archive = archiver("zip", { + zlib: { level: 9 }, + }); + + // Set up an event listener for the 'close' event to resolve the promise when the archiver has finalized. + output.on("close", function () { + logger.log("info", "Archiver has finalized. The output and the file descriptor have closed."); + resolve(); + }); + + // Set up an event listener for the 'warning' event to handle warnings appropriately, logging them or rejecting + // the promise based on the error code. + archive.on("warning", function (err) { + if (err.code === "ENOENT") { + logger.log("warn", `Archiver issued a warning: ${err.code} - ${err.message}`); + } else { + reject(err); + } + }); + + // Set up an event listener for the 'error' event to reject the promise if any error occurs during archiving. + archive.on("error", function (err) { + reject(err); + }); + + // Pipe archive data to the file. + archive.pipe(output); + + // Add the directory to the archive, under the provided directory name. + archive.directory(directoryToZip, containerDirName); + + // Finalize the archive, indicating that no more files will be added and triggering the 'close' event once all + // data has been written. + archive.finalize(); + }); +} + +// Engage! +main(); diff --git a/TypeScript/10ScopesAndTypes/package.json b/TypeScript/10ScopesAndTypes/package.json index 1731a9b..7001124 100644 --- a/TypeScript/10ScopesAndTypes/package.json +++ b/TypeScript/10ScopesAndTypes/package.json @@ -7,17 +7,20 @@ "akiVersion": "~3.7", "scripts": { "setup": "npm i", - "build": "node ./packageBuild.ts" + "build": "node ./build.mjs", + "buildinfo": "node ./build.mjs --verbose" }, "devDependencies": { "@types/node": "20.4.5", "@typescript-eslint/eslint-plugin": "6.2.0", "@typescript-eslint/parser": "6.2.0", - "bestzip": "2.2.1", + "archiver": "^6.0", "eslint": "8.46.0", - "fs-extra": "11.1.1", - "glob": "10.3.3", + "fs-extra": "^11.1", + "ignore": "^5.2", + "os": "^0.1", "tsyringe": "4.8.0", - "typescript": "5.1.6" + "typescript": "5.1.6", + "winston": "^3.9" } -} \ No newline at end of file +} diff --git a/TypeScript/10ScopesAndTypes/packageBuild.ts b/TypeScript/10ScopesAndTypes/packageBuild.ts deleted file mode 100644 index d76ddd8..0000000 --- a/TypeScript/10ScopesAndTypes/packageBuild.ts +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env node - -// This is a simple script used to build a mod package. The script will copy necessary files to the build directory -// and compress the build directory into a zip file that can be easily shared. - -const fs = require("fs-extra"); -const glob = require("glob"); -const zip = require('bestzip'); -const path = require("path"); -const minimatch = require('minimatch'); - -// Load the package.json file to get some information about the package so we can name things appropriately. This is -// atypical, and you would never do this in a production environment, but this script is only used for development so -// it's fine in this case. Some of these values are stored in environment variables, but those differ between node -// versions; the 'author' value is not available after node v14. -const { author, name:packageName, version } = require("./package.json"); - -// Generate the name of the package, stripping out all non-alphanumeric characters in the 'author' and 'name'. -const modName = `${author.replace(/[^a-z0-9]/gi, "")}-${packageName.replace(/[^a-z0-9]/gi, "")}-${version}`; -console.log(`Generated package name: ${modName}`); - -// Delete the old build directory and compressed package file. -fs.rmSync(`${__dirname}/dist`, { force: true, recursive: true }); -console.log("Previous build files deleted."); - -// Generate a list of files that should not be copied over into the distribution directory. This is a blacklist to ensure -// we always copy over additional files and directories that authors may have added to their project. This may need to be -// expanded upon by the mod author to allow for node modules that are used within the mod; example commented out below. -const ignoreList = [ - "node_modules/", - // "node_modules/!(weighted|glob)", // Instead of excluding the entire node_modules directory, allow two node modules. - "src/**/*.js", - "types/", - ".git/", - ".gitea/", - ".eslintignore", - ".eslintrc.json", - ".gitignore", - ".DS_Store", - "packageBuild.ts", - "mod.code-workspace", - "package-lock.json", - "tsconfig.json" -]; -const exclude = glob.sync(`{${ignoreList.join(",")}}`, { realpath: true, dot: true }); - -fs.copySync(__dirname, path.normalize(`${__dirname}/../~${modName}`), {filter: (src) => -{ - const relativePath = path.relative(__dirname, src); - const shouldExclude = exclude.some((pattern) => minimatch(relativePath, pattern)); - console.log(`${relativePath} - Excluded: ${shouldExclude}`); - return !shouldExclude; -},}); - -fs.moveSync(path.normalize(`${__dirname}/../~${modName}`), path.normalize(`${__dirname}/${modName}`), { overwrite: true }); -fs.copySync(path.normalize(`${__dirname}/${modName}`), path.normalize(`${__dirname}/dist`)); -console.log("Build files copied."); - -// Compress the files for easy distribution. The compressed file is saved into the dist directory. When uncompressed we -// need to be sure that it includes a directory that the user can easily copy into their game mods directory. -zip({ - source: modName, - destination: `dist/${modName}.zip`, - cwd: __dirname -}).catch(function(err) -{ - console.error("A bestzip error has occurred: ", err.stack); -}).then(function() -{ - console.log(`Compressed mod package to: /dist/${modName}.zip`); - - // Now that we're done with the compression we can delete the temporary build directory. - fs.rmSync(`${__dirname}/${modName}`, { force: true, recursive: true }); - console.log("Build successful! your zip file has been created and is ready to be uploaded to hub.sp-tarkov.com/files/"); -}); \ No newline at end of file diff --git a/TypeScript/11BundleLoadingSample/.buildignore b/TypeScript/11BundleLoadingSample/.buildignore new file mode 100644 index 0000000..2cbde65 --- /dev/null +++ b/TypeScript/11BundleLoadingSample/.buildignore @@ -0,0 +1,20 @@ +/.buildignore +/.DS_Store +/.editorconfig +/.eslintignore +/.eslintrc.json +/.git +/.github +/.gitignore +/.gitlab +/.nvmrc +/.prettierrc +/.vscode +/build.mjs +/dist +/images +/mod.code-workspace +/node_modules +/package-lock.json +/tsconfig.json +/types diff --git a/TypeScript/11BundleLoadingSample/.eslintrc.json b/TypeScript/11BundleLoadingSample/.eslintrc.json index c505160..071a313 100644 --- a/TypeScript/11BundleLoadingSample/.eslintrc.json +++ b/TypeScript/11BundleLoadingSample/.eslintrc.json @@ -1,75 +1,98 @@ { - "root": true, - "parser": "@typescript-eslint/parser", - "plugins": [ - "@typescript-eslint" - ], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended" - ], - "rules": { - "@typescript-eslint/no-explicit-any": 0, - "@typescript-eslint/no-unused-vars": 1, - "@typescript-eslint/no-empty-interface": 0, - "@typescript-eslint/no-namespace": 0, - "@typescript-eslint/comma-dangle": 1, - "@typescript-eslint/func-call-spacing": 2, - "@typescript-eslint/quotes": 1, - "@typescript-eslint/brace-style": [ - "warn", - "allman" + "root": true, + "parser": "@typescript-eslint/parser", + "plugins": [ + "@typescript-eslint" ], - "@typescript-eslint/naming-convention": [ - "warn", - { - "selector": "default", - "format": ["camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "typeLike", - "format": ["PascalCase"] - }, - { - "selector": "objectLiteralProperty", - "format": ["PascalCase", "camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "typeProperty", - "format": ["PascalCase", "camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "enumMember", - "format": ["UPPER_CASE"] - } + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" ], - "@typescript-eslint/indent": [ - "warn", - 4 - ], - "@typescript-eslint/no-unused-expressions": [ - "warn", - { - "allowShortCircuit": false, - "allowTernary": false - } - ], - "@typescript-eslint/keyword-spacing": [ - "warn", - { - "before": true, - "after": true - } - ], - "@typescript-eslint/explicit-module-boundary-types": [ - "warn", - { - "allowArgumentsExplicitlyTypedAsAny": true - } + "rules": { + "@typescript-eslint/no-explicit-any": 0, + "@typescript-eslint/no-unused-vars": 1, + "@typescript-eslint/no-empty-interface": 0, + "@typescript-eslint/no-namespace": 0, + "@typescript-eslint/comma-dangle": 1, + "@typescript-eslint/func-call-spacing": 2, + "@typescript-eslint/quotes": 1, + "@typescript-eslint/brace-style": [ + "warn", + "allman" + ], + "@typescript-eslint/naming-convention": [ + "warn", + { + "selector": "default", + "format": [ + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "typeLike", + "format": [ + "PascalCase" + ] + }, + { + "selector": "objectLiteralProperty", + "format": [ + "PascalCase", + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "typeProperty", + "format": [ + "PascalCase", + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "enumMember", + "format": [ + "UPPER_CASE" + ] + } + ], + "@typescript-eslint/indent": [ + "warn", + 4 + ], + "@typescript-eslint/no-unused-expressions": [ + "warn", + { + "allowShortCircuit": false, + "allowTernary": false + } + ], + "@typescript-eslint/keyword-spacing": [ + "warn", + { + "before": true, + "after": true + } + ], + "@typescript-eslint/explicit-module-boundary-types": [ + "warn", + { + "allowArgumentsExplicitlyTypedAsAny": true + } + ] + }, + "overrides": [ + { + "files": [ + "*.mjs", + "*.ts" + ], + "env": { + "node": true + } + } ] - } } \ No newline at end of file diff --git a/TypeScript/11BundleLoadingSample/README.md b/TypeScript/11BundleLoadingSample/README.md index 434d063..8cba28e 100644 --- a/TypeScript/11BundleLoadingSample/README.md +++ b/TypeScript/11BundleLoadingSample/README.md @@ -1,64 +1,66 @@ +# Welcome to the SPT-AKI Modding Project -This project was created to automate most parts of building and setting up an environment. +This project is designed to streamline the initial setup process for building and creating mods in the SPT-AKI environment. Follow this guide to set up your environment efficiently. -## **NodeJS:** +## **Table of Contents** +- [NodeJS Setup](#nodejs-setup) +- [IDE Setup](#ide-setup) +- [Workspace Configuration](#workspace-configuration) +- [Environment Setup](#environment-setup) +- [Essential Concepts](#essential-concepts) +- [Coding Guidelines](#coding-guidelines) +- [Distribution Guidelines](#distribution-guidelines) -The first step would be to install nodejs on your pc, the version you NEED is **16.17.1** +## **NodeJS Setup** -That version is the one that has been used to test the mod templates and build scripts. +Before you begin, ensure to install NodeJS version `v16.17.1`, which has been tested thoroughly with our mod templates and build scripts. Download it from the [official NodeJS website](https://nodejs.org/). -It can be downloaded from here: https://nodejs.org/dist/v16.17.1/node-v16.17.1-x64.msi +After installation, it's advised to reboot your system. -A system reboot may be needed after install. +## **IDE Setup** -## **IDE:** +For this project, you can work with either [VSCodium](https://vscodium.com/) or [VSCode](https://code.visualstudio.com/). However, we strongly recommend using VSCode, as all development and testing have been carried out using this IDE, ensuring a smoother experience and compatibility with the project setups. Either way, we have a prepared a workspace file to assist you in setting up your environment. -The second step is having an IDE ready. We've setup a VSCodium workspace file to help with this. +## **Workspace Configuration** -You CAN use Visual Studio Code if you so desire, just keep in mind that our devs tests on the mod files was done using VSCode. +With NodeJS and your chosen IDE ready, initiate the `mod.code-workspace` file using your IDE: -You can get VSCode here: https://code.visualstudio.com/ +> File -> Open Workspace from File... -## **Workspace:** +Upon project loading, consider installing recommended plugins like the ESLint plugin. -Once you have NodeJS and VSCode/VSCodium ready, open the mod.code-workspace file with VSCode (File->Open Workspace from File...). +## **Environment Setup** -Once the project loads you may be recommended to install the ESLint plugin. This is HIGHLY recommended. +An automated task is available to configure your environment for Typescript utilization: -## **Environment Setup:** +> Terminal -> Run Task... -> Show All Tasks... -> npm: install -There is a task that will automatically setup your environment to use typescript. +Note: Preserve the `node_modules` folder as it contains necessary dependencies for Typescript and other functionalities. -To run it, you just need to go to: +## **Essential Concepts** -> Terminal->Run Task...->Show All Tasks...->npm: install +Prioritize understanding Dependency Injection and Inversion of Control, the architectural principles SPT-AKI adopts. Comprehensive guidelines will be available on the hub upon release. -After running this task, your environment will be ready to start coding. +Some resources to get you started: + - [A quick intro to Dependency Injection](https://www.freecodecamp.org/news/a-quick-intro-to-dependency-injection-what-it-is-and-when-to-use-it-7578c84fa88f/) + - [Understanding Inversion of Control (IoC) Principle](https://medium.com/@amitkma/understanding-inversion-of-control-ioc-principle-163b1dc97454) -DO NOT remove the node_modules folder, this is an auto generated directory that has the required dependencies to be able to use typescript and more. +## **Coding Guidelines** -## **IMPORTANT:** +Focus your mod development around the `mod.ts` file. In the `package.json` file, only alter these properties: `"name"`, `"version"`, `"license"`, `"author"`, and `"akiVersion"`. -Before starting to work on your mod, we suggest you read about Dependency Injection and Inversion of Control as this is the adopted architecture SPT-AKI has adopted. +New to Typescript? Find comprehensive documentation on the [official website](https://www.typescriptlang.org/docs/). -It will be difficult to understand some of the problems you may be having if you dont understand the basics of it. +## **Distribution Guidelines** -A guide explaining all the essentials will be available on the hub on release for you to read about. +Automated tasks are set up to bundle all necessary files for your mod to function in SPT-AKI: -## **Coding:** +> Terminal -> Run Task... -> Show All Tasks... -> npm: build -All your work should be centered around the mod.ts file as an entry point. -You can ONLY change the following properties from the package.json file: `"name"`, `"version"`, `"license"`: `"MIT"`, `"author"`, `"akiVersion"`. +The ZIP output, located in the `dist` directory, contains all required files. Ensure all files are included and modify the `.buildignore` file as needed. This ZIP file is your uploadable asset for the hub. -If you have never used typescript before, you can read about it here: https://www.typescriptlang.org/docs/ +## **Conclusion** -## **Distributing your mod:** +With this setup, you're ready to begin modding with SPT-AKI. If you run into any trouble be sure to check out the [modding documentation on the hub](https://hub.sp-tarkov.com/doc/lexicon/66-modding/). If you really get stuck feel free to join us in the [#mods-development](https://discord.com/channels/875684761291599922/875803116409323562) official Discord channel. -The project has been set up with an automatic task that will copy and zip ALL required files for your mod to work on SPT-AKI. -To run this task you just need to go to: - -> Terminal->Run Task...->Show All Tasks...->npm: build:zip - -The output will be a mod.zip file that will appear on the root of the project. - -Always verify that all files were included into the zip file. \ No newline at end of file +Build something awesome! diff --git a/TypeScript/11BundleLoadingSample/build.mjs b/TypeScript/11BundleLoadingSample/build.mjs new file mode 100644 index 0000000..d00117c --- /dev/null +++ b/TypeScript/11BundleLoadingSample/build.mjs @@ -0,0 +1,383 @@ +#!/usr/bin/env node + +/** + * Build Script + * + * This script automates the build process for server-side SPT mod projects, facilitating the creation of distributable + * mod packages. It performs a series of operations as outlined below: + * - Loads the .buildignore file, which is used to list files that should be ignored during the build process. + * - Loads the package.json to get project details so a descriptive name can be created for the mod package. + * - Creates a distribution directory and a temporary working directory. + * - Copies files to the temporary directory while respecting the .buildignore rules. + * - Creates a zip archive of the project files. + * - Moves the zip file to the root of the distribution directory. + * - Cleans up the temporary directory. + * + * It's typical that this script be customized to suit the needs of each project. For example, the script can be updated + * to perform additional operations, such as moving the mod package to a specific location or uploading it to a server. + * This script is intended to be a starting point for developers to build upon. + * + * Usage: + * - Run this script using npm: `npm run build` + * - Use `npm run buildinfo` for detailed logging. + * + * Note: + * - Ensure that all necessary Node.js modules are installed before running the script: `npm install` + * - The script reads configurations from the `package.json` and `.buildignore` files; ensure they are correctly set up. + * + * @author Refringe + * @version v1.0.0 + */ + +import fs from "fs-extra"; +import os from "os"; +import path from "path"; +import { fileURLToPath } from "url"; +import { dirname } from "path"; +import ignore from "ignore"; +import archiver from "archiver"; +import winston from "winston"; + +// Get the command line arguments to determine whether to use verbose logging. +const args = process.argv.slice(2); +const verbose = args.includes("--verbose") || args.includes("-v"); + +// Configure the Winston logger to use colours. +const logColors = { + error: "red", + warn: "yellow", + info: "grey", + success: "green", +}; +winston.addColors(logColors); + +// Create a logger instance to log build progress. Configure the logger levels to allow for different levels of logging +// based on the verbosity flag, and set the console transport to log messages of the appropriate level. +const logger = winston.createLogger({ + levels: { + error: 0, + warn: 1, + success: 2, + info: 3, + }, + format: winston.format.combine( + winston.format.colorize(), + winston.format.printf(info => { + return `${info.level}: ${info.message}`; + }) + ), + transports: [ + new winston.transports.Console({ + level: verbose ? "info" : "success", + }), + ], +}); + +/** + * The main function orchestrates the build process for creating a distributable mod package. It leverages a series of + * helper functions to perform various tasks such as loading configuration files, setting up directories, copying files + * according to `.buildignore` rules, and creating a ZIP archive of the project files. + * + * Utilizes the Winston logger to provide information on the build status at different stages of the process. + * + * @returns {void} + */ +async function main() { + // Get the current directory where the script is being executed + const currentDir = getCurrentDirectory(); + + // Defining at this scope because we need to use it in the finally block. + let projectDir; + + try { + // Load the .buildignore file to set up an ignore handler for the build process. + const buildIgnorePatterns = await loadBuildIgnoreFile(currentDir); + + // Load the package.json file to get project details. + const packageJson = await loadPackageJson(currentDir); + + // Create a descriptive name for the mod package. + const projectName = createProjectName(packageJson); + logger.log("success", `Project name created: ${projectName}`); + + // Remove the old distribution directory and create a fresh one. + const distDir = await removeOldDistDirectory(currentDir); + logger.log("info", "Distribution directory successfully cleaned."); + + // Create a temporary working directory to perform the build operations. + projectDir = await createTemporaryDirectoryWithProjectName(projectName); + logger.log("success", "Temporary working directory successfully created."); + logger.log("info", projectDir); + + // Copy files to the temporary directory while respecting the .buildignore rules. + logger.log("info", "Beginning copy operation using .buildignore file..."); + await copyFiles(currentDir, projectDir, buildIgnorePatterns); + logger.log("success", "Files successfully copied to temporary directory."); + + // Create a zip archive of the project files. + logger.log("info", "Beginning folder compression..."); + const zipFilePath = path.join(path.dirname(projectDir), `${projectName}.zip`); + await createZipFile(projectDir, zipFilePath, "user/mods/" + projectName); + logger.log("success", "Archive successfully created."); + logger.log("info", zipFilePath); + + // Move the zip file inside of the project directory, within the temporary working directory. + const zipFileInProjectDir = path.join(projectDir, `${projectName}.zip`); + await fs.move(zipFilePath, zipFileInProjectDir); + logger.log("success", "Archive successfully moved."); + logger.log("info", zipFileInProjectDir); + + // Move the temporary directory into the distribution directory. + await fs.move(projectDir, distDir); + logger.log("success", "Temporary directory successfully moved into project distribution directory."); + + // Log the success message. Write out the path to the mod package. + logger.log("success", "------------------------------------"); + logger.log("success", "Build script completed successfully!"); + logger.log("success", "Your mod package has been created in the 'dist' directory:"); + logger.log("success", `/${path.relative(process.cwd(), path.join(distDir, `${projectName}.zip`))}`); + logger.log("success", "------------------------------------"); + if (!verbose) { + logger.log("success", "To see a detailed build log, use `npm run buildinfo`."); + logger.log("success", "------------------------------------"); + } + } catch (err) { + // If any of the file operations fail, log the error. + logger.log("error", "An error occurred: " + err); + } finally { + // Clean up the temporary directory, even if the build fails. + if (projectDir) { + try { + await fs.promises.rm(projectDir, { force: true, recursive: true }); + logger.log("info", "Cleaned temporary directory."); + } catch (err) { + logger.log("error", "Failed to clean temporary directory: " + err); + } + } + } +} + +/** + * Retrieves the current working directory where the script is being executed. This directory is used as a reference + * point for various file operations throughout the build process, ensuring that paths are resolved correctly regardless + * of the location from which the script is invoked. + * + * @returns {string} The absolute path of the current working directory. + */ +function getCurrentDirectory() { + return dirname(fileURLToPath(import.meta.url)); +} + +/** + * Loads the `.buildignore` file and sets up an ignore handler using the `ignore` module. The `.buildignore` file + * contains a list of patterns describing files and directories that should be ignored during the build process. The + * ignore handler created by this method is used to filter files and directories when copying them to the temporary + * directory, ensuring that only necessary files are included in the final mod package. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to an ignore handler. + */ +async function loadBuildIgnoreFile(currentDir) { + const buildIgnorePath = path.join(currentDir, ".buildignore"); + + try { + // Attempt to read the contents of the .buildignore file asynchronously. + const fileContent = await fs.promises.readFile(buildIgnorePath, "utf-8"); + + // Return a new ignore instance and add the rules from the .buildignore file (split by newlines). + return ignore().add(fileContent.split("\n")); + } catch (err) { + logger.log("warn", "Failed to read .buildignore file. No files or directories will be ignored."); + + // Return an empty ignore instance, ensuring the build process can continue. + return ignore(); + } +} + +/** + * Loads the `package.json` file and returns its content as a JSON object. The `package.json` file contains important + * project details such as the name and version, which are used in later stages of the build process to create a + * descriptive name for the mod package. The method reads the file from the current working directory, ensuring that it + * accurately reflects the current state of the project. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to a JSON object containing the contents of the `package.json`. + */ +async function loadPackageJson(currentDir) { + const packageJsonPath = path.join(currentDir, "package.json"); + + // Read the contents of the package.json file asynchronously as a UTF-8 string. + const packageJsonContent = await fs.promises.readFile(packageJsonPath, "utf-8"); + + return JSON.parse(packageJsonContent); +} + +/** + * Constructs a descriptive name for the mod package using details from the `package.json` file. The name is created by + * concatenating the project name, version, and a timestamp, resulting in a unique and descriptive file name for each + * build. This name is used as the base name for the temporary working directory and the final ZIP archive, helping to + * identify different versions of the mod package easily. + * + * @param {Object} packageJson - A JSON object containing the contents of the `package.json` file. + * @returns {string} A string representing the constructed project name. + */ +function createProjectName(packageJson) { + // Remove any non-alphanumeric characters from the author and name. + const author = packageJson.author.replace(/\W/g, ""); + const name = packageJson.name.replace(/\W/g, ""); + const version = packageJson.version; + + // Ensure the name is lowercase, as per the package.json specification. + return `${author}-${name}-${version}`.toLowerCase(); +} + +/** + * Defines the location of the distribution directory where the final mod package will be stored and deletes any + * existing distribution directory to ensure a clean slate for the build process. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to the absolute path to the distribution directory. + */ +async function removeOldDistDirectory(projectDir) { + const distPath = path.join(projectDir, "dist"); + await fs.remove(distPath); + return distPath; +} + +/** + * Creates a temporary working directory using the project name. This directory serves as a staging area where project + * files are gathered before being archived into the final mod package. The method constructs a unique directory path + * by appending the project name to a base temporary directory path, ensuring that each build has its own isolated + * working space. This approach facilitates clean and organized build processes, avoiding potential conflicts with other + * builds. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @param {string} projectName - The constructed project name, used to create a unique path for the temporary directory. + * @returns {Promise} A promise that resolves to the absolute path of the newly created temporary directory. + */ +async function createTemporaryDirectoryWithProjectName(projectName) { + // Create a new directory in the system's temporary folder to hold the project files. + const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "spt-mod-build-")); + + // Create a subdirectory within the temporary directory using the project name for this specific build. + const projectDir = path.join(tempDir, projectName); + await fs.ensureDir(projectDir); + + return projectDir; +} + +/** + * Copies the project files to the temporary directory while respecting the rules defined in the `.buildignore` file. + * The method is recursive, iterating over all files and directories in the source directory and using the ignore + * handler to filter out files and directories that match the patterns defined in the `.buildignore` file. This ensures + * that only the necessary files are included in the final mod package, adhering to the specifications defined by the + * developer in the `.buildignore` file. + * + * The copy operations are delayed and executed in parallel to improve efficiency and reduce the build time. This is + * achieved by creating an array of copy promises and awaiting them all at the end of the function. + * + * @param {string} sourceDirectory - The absolute path of the current working directory. + * @param {string} destinationDirectory - The absolute path of the temporary directory where the files will be copied. + * @param {Ignore} ignoreHandler - The ignore handler created from the `.buildignore` file. + * @returns {Promise} A promise that resolves when all copy operations are completed successfully. + */ +async function copyFiles(srcDir, destDir, ignoreHandler) { + try { + // Read the contents of the source directory to get a list of entries (files and directories). + const entries = await fs.promises.readdir(srcDir, { withFileTypes: true }); + + // Initialize an array to hold the promises returned by recursive calls to copyFiles and copyFile operations. + const copyOperations = []; + + for (const entry of entries) { + // Define the source and destination paths for each entry. + const srcPath = path.join(srcDir, entry.name); + const destPath = path.join(destDir, entry.name); + + // Get the relative path of the source file to check against the ignore handler. + const relativePath = path.relative(process.cwd(), srcPath); + + // If the ignore handler dictates that this file should be ignored, skip to the next iteration. + if (ignoreHandler.ignores(relativePath)) { + logger.log("info", `Ignored: /${path.relative(process.cwd(), srcPath)}`); + continue; + } + + if (entry.isDirectory()) { + // If the entry is a directory, create the corresponding temporary directory and make a recursive call + // to copyFiles to handle copying the contents of the directory. + await fs.ensureDir(destPath); + copyOperations.push(copyFiles(srcPath, destPath, ignoreHandler)); + } else { + // If the entry is a file, add a copyFile operation to the copyOperations array and log the event when + // the operation is successful. + copyOperations.push( + fs.copy(srcPath, destPath).then(() => { + logger.log("info", `Copied: /${path.relative(process.cwd(), srcPath)}`); + }) + ); + } + } + + // Await all copy operations to ensure all files and directories are copied before exiting the function. + await Promise.all(copyOperations); + } catch (err) { + // Log an error message if any error occurs during the copy process. + logger.log("error", "Error copying files: " + err); + } +} + +/** + * Creates a ZIP archive of the project files located in the temporary directory. The method uses the `archiver` module + * to create a ZIP file, which includes all the files that have been copied to the temporary directory during the build + * process. The ZIP file is named using the project name, helping to identify the contents of the archive easily. + * + * @param {string} directoryPath - The absolute path of the temporary directory containing the project files. + * @param {string} projectName - The constructed project name, used to name the ZIP file. + * @returns {Promise} A promise that resolves to the absolute path of the created ZIP file. + */ +async function createZipFile(directoryToZip, zipFilePath, containerDirName) { + return new Promise((resolve, reject) => { + // Create a write stream to the specified ZIP file path. + const output = fs.createWriteStream(zipFilePath); + + // Create a new archiver instance with ZIP format and maximum compression level. + const archive = archiver("zip", { + zlib: { level: 9 }, + }); + + // Set up an event listener for the 'close' event to resolve the promise when the archiver has finalized. + output.on("close", function () { + logger.log("info", "Archiver has finalized. The output and the file descriptor have closed."); + resolve(); + }); + + // Set up an event listener for the 'warning' event to handle warnings appropriately, logging them or rejecting + // the promise based on the error code. + archive.on("warning", function (err) { + if (err.code === "ENOENT") { + logger.log("warn", `Archiver issued a warning: ${err.code} - ${err.message}`); + } else { + reject(err); + } + }); + + // Set up an event listener for the 'error' event to reject the promise if any error occurs during archiving. + archive.on("error", function (err) { + reject(err); + }); + + // Pipe archive data to the file. + archive.pipe(output); + + // Add the directory to the archive, under the provided directory name. + archive.directory(directoryToZip, containerDirName); + + // Finalize the archive, indicating that no more files will be added and triggering the 'close' event once all + // data has been written. + archive.finalize(); + }); +} + +// Engage! +main(); diff --git a/TypeScript/11BundleLoadingSample/package.json b/TypeScript/11BundleLoadingSample/package.json index 4a3a26f..54e4313 100644 --- a/TypeScript/11BundleLoadingSample/package.json +++ b/TypeScript/11BundleLoadingSample/package.json @@ -8,17 +8,20 @@ "akiVersion": "~3.7", "scripts": { "setup": "npm i", - "build": "node ./packageBuild.ts" + "build": "node ./build.mjs", + "buildinfo": "node ./build.mjs --verbose" }, "devDependencies": { "@types/node": "20.4.5", "@typescript-eslint/eslint-plugin": "6.2.0", "@typescript-eslint/parser": "6.2.0", - "bestzip": "2.2.1", + "archiver": "^6.0", "eslint": "8.46.0", - "fs-extra": "11.1.1", - "glob": "10.3.3", + "fs-extra": "^11.1", + "ignore": "^5.2", + "os": "^0.1", "tsyringe": "4.8.0", - "typescript": "5.1.6" + "typescript": "5.1.6", + "winston": "^3.9" } -} \ No newline at end of file +} diff --git a/TypeScript/11BundleLoadingSample/packageBuild.ts b/TypeScript/11BundleLoadingSample/packageBuild.ts deleted file mode 100644 index d76ddd8..0000000 --- a/TypeScript/11BundleLoadingSample/packageBuild.ts +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env node - -// This is a simple script used to build a mod package. The script will copy necessary files to the build directory -// and compress the build directory into a zip file that can be easily shared. - -const fs = require("fs-extra"); -const glob = require("glob"); -const zip = require('bestzip'); -const path = require("path"); -const minimatch = require('minimatch'); - -// Load the package.json file to get some information about the package so we can name things appropriately. This is -// atypical, and you would never do this in a production environment, but this script is only used for development so -// it's fine in this case. Some of these values are stored in environment variables, but those differ between node -// versions; the 'author' value is not available after node v14. -const { author, name:packageName, version } = require("./package.json"); - -// Generate the name of the package, stripping out all non-alphanumeric characters in the 'author' and 'name'. -const modName = `${author.replace(/[^a-z0-9]/gi, "")}-${packageName.replace(/[^a-z0-9]/gi, "")}-${version}`; -console.log(`Generated package name: ${modName}`); - -// Delete the old build directory and compressed package file. -fs.rmSync(`${__dirname}/dist`, { force: true, recursive: true }); -console.log("Previous build files deleted."); - -// Generate a list of files that should not be copied over into the distribution directory. This is a blacklist to ensure -// we always copy over additional files and directories that authors may have added to their project. This may need to be -// expanded upon by the mod author to allow for node modules that are used within the mod; example commented out below. -const ignoreList = [ - "node_modules/", - // "node_modules/!(weighted|glob)", // Instead of excluding the entire node_modules directory, allow two node modules. - "src/**/*.js", - "types/", - ".git/", - ".gitea/", - ".eslintignore", - ".eslintrc.json", - ".gitignore", - ".DS_Store", - "packageBuild.ts", - "mod.code-workspace", - "package-lock.json", - "tsconfig.json" -]; -const exclude = glob.sync(`{${ignoreList.join(",")}}`, { realpath: true, dot: true }); - -fs.copySync(__dirname, path.normalize(`${__dirname}/../~${modName}`), {filter: (src) => -{ - const relativePath = path.relative(__dirname, src); - const shouldExclude = exclude.some((pattern) => minimatch(relativePath, pattern)); - console.log(`${relativePath} - Excluded: ${shouldExclude}`); - return !shouldExclude; -},}); - -fs.moveSync(path.normalize(`${__dirname}/../~${modName}`), path.normalize(`${__dirname}/${modName}`), { overwrite: true }); -fs.copySync(path.normalize(`${__dirname}/${modName}`), path.normalize(`${__dirname}/dist`)); -console.log("Build files copied."); - -// Compress the files for easy distribution. The compressed file is saved into the dist directory. When uncompressed we -// need to be sure that it includes a directory that the user can easily copy into their game mods directory. -zip({ - source: modName, - destination: `dist/${modName}.zip`, - cwd: __dirname -}).catch(function(err) -{ - console.error("A bestzip error has occurred: ", err.stack); -}).then(function() -{ - console.log(`Compressed mod package to: /dist/${modName}.zip`); - - // Now that we're done with the compression we can delete the temporary build directory. - fs.rmSync(`${__dirname}/${modName}`, { force: true, recursive: true }); - console.log("Build successful! your zip file has been created and is ready to be uploaded to hub.sp-tarkov.com/files/"); -}); \ No newline at end of file diff --git a/TypeScript/12ClassExtensionOverride/.buildignore b/TypeScript/12ClassExtensionOverride/.buildignore new file mode 100644 index 0000000..2cbde65 --- /dev/null +++ b/TypeScript/12ClassExtensionOverride/.buildignore @@ -0,0 +1,20 @@ +/.buildignore +/.DS_Store +/.editorconfig +/.eslintignore +/.eslintrc.json +/.git +/.github +/.gitignore +/.gitlab +/.nvmrc +/.prettierrc +/.vscode +/build.mjs +/dist +/images +/mod.code-workspace +/node_modules +/package-lock.json +/tsconfig.json +/types diff --git a/TypeScript/12ClassExtensionOverride/.eslintrc.json b/TypeScript/12ClassExtensionOverride/.eslintrc.json index c505160..071a313 100644 --- a/TypeScript/12ClassExtensionOverride/.eslintrc.json +++ b/TypeScript/12ClassExtensionOverride/.eslintrc.json @@ -1,75 +1,98 @@ { - "root": true, - "parser": "@typescript-eslint/parser", - "plugins": [ - "@typescript-eslint" - ], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended" - ], - "rules": { - "@typescript-eslint/no-explicit-any": 0, - "@typescript-eslint/no-unused-vars": 1, - "@typescript-eslint/no-empty-interface": 0, - "@typescript-eslint/no-namespace": 0, - "@typescript-eslint/comma-dangle": 1, - "@typescript-eslint/func-call-spacing": 2, - "@typescript-eslint/quotes": 1, - "@typescript-eslint/brace-style": [ - "warn", - "allman" + "root": true, + "parser": "@typescript-eslint/parser", + "plugins": [ + "@typescript-eslint" ], - "@typescript-eslint/naming-convention": [ - "warn", - { - "selector": "default", - "format": ["camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "typeLike", - "format": ["PascalCase"] - }, - { - "selector": "objectLiteralProperty", - "format": ["PascalCase", "camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "typeProperty", - "format": ["PascalCase", "camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "enumMember", - "format": ["UPPER_CASE"] - } + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" ], - "@typescript-eslint/indent": [ - "warn", - 4 - ], - "@typescript-eslint/no-unused-expressions": [ - "warn", - { - "allowShortCircuit": false, - "allowTernary": false - } - ], - "@typescript-eslint/keyword-spacing": [ - "warn", - { - "before": true, - "after": true - } - ], - "@typescript-eslint/explicit-module-boundary-types": [ - "warn", - { - "allowArgumentsExplicitlyTypedAsAny": true - } + "rules": { + "@typescript-eslint/no-explicit-any": 0, + "@typescript-eslint/no-unused-vars": 1, + "@typescript-eslint/no-empty-interface": 0, + "@typescript-eslint/no-namespace": 0, + "@typescript-eslint/comma-dangle": 1, + "@typescript-eslint/func-call-spacing": 2, + "@typescript-eslint/quotes": 1, + "@typescript-eslint/brace-style": [ + "warn", + "allman" + ], + "@typescript-eslint/naming-convention": [ + "warn", + { + "selector": "default", + "format": [ + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "typeLike", + "format": [ + "PascalCase" + ] + }, + { + "selector": "objectLiteralProperty", + "format": [ + "PascalCase", + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "typeProperty", + "format": [ + "PascalCase", + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "enumMember", + "format": [ + "UPPER_CASE" + ] + } + ], + "@typescript-eslint/indent": [ + "warn", + 4 + ], + "@typescript-eslint/no-unused-expressions": [ + "warn", + { + "allowShortCircuit": false, + "allowTernary": false + } + ], + "@typescript-eslint/keyword-spacing": [ + "warn", + { + "before": true, + "after": true + } + ], + "@typescript-eslint/explicit-module-boundary-types": [ + "warn", + { + "allowArgumentsExplicitlyTypedAsAny": true + } + ] + }, + "overrides": [ + { + "files": [ + "*.mjs", + "*.ts" + ], + "env": { + "node": true + } + } ] - } } \ No newline at end of file diff --git a/TypeScript/12ClassExtensionOverride/README.md b/TypeScript/12ClassExtensionOverride/README.md index 434d063..8cba28e 100644 --- a/TypeScript/12ClassExtensionOverride/README.md +++ b/TypeScript/12ClassExtensionOverride/README.md @@ -1,64 +1,66 @@ +# Welcome to the SPT-AKI Modding Project -This project was created to automate most parts of building and setting up an environment. +This project is designed to streamline the initial setup process for building and creating mods in the SPT-AKI environment. Follow this guide to set up your environment efficiently. -## **NodeJS:** +## **Table of Contents** +- [NodeJS Setup](#nodejs-setup) +- [IDE Setup](#ide-setup) +- [Workspace Configuration](#workspace-configuration) +- [Environment Setup](#environment-setup) +- [Essential Concepts](#essential-concepts) +- [Coding Guidelines](#coding-guidelines) +- [Distribution Guidelines](#distribution-guidelines) -The first step would be to install nodejs on your pc, the version you NEED is **16.17.1** +## **NodeJS Setup** -That version is the one that has been used to test the mod templates and build scripts. +Before you begin, ensure to install NodeJS version `v16.17.1`, which has been tested thoroughly with our mod templates and build scripts. Download it from the [official NodeJS website](https://nodejs.org/). -It can be downloaded from here: https://nodejs.org/dist/v16.17.1/node-v16.17.1-x64.msi +After installation, it's advised to reboot your system. -A system reboot may be needed after install. +## **IDE Setup** -## **IDE:** +For this project, you can work with either [VSCodium](https://vscodium.com/) or [VSCode](https://code.visualstudio.com/). However, we strongly recommend using VSCode, as all development and testing have been carried out using this IDE, ensuring a smoother experience and compatibility with the project setups. Either way, we have a prepared a workspace file to assist you in setting up your environment. -The second step is having an IDE ready. We've setup a VSCodium workspace file to help with this. +## **Workspace Configuration** -You CAN use Visual Studio Code if you so desire, just keep in mind that our devs tests on the mod files was done using VSCode. +With NodeJS and your chosen IDE ready, initiate the `mod.code-workspace` file using your IDE: -You can get VSCode here: https://code.visualstudio.com/ +> File -> Open Workspace from File... -## **Workspace:** +Upon project loading, consider installing recommended plugins like the ESLint plugin. -Once you have NodeJS and VSCode/VSCodium ready, open the mod.code-workspace file with VSCode (File->Open Workspace from File...). +## **Environment Setup** -Once the project loads you may be recommended to install the ESLint plugin. This is HIGHLY recommended. +An automated task is available to configure your environment for Typescript utilization: -## **Environment Setup:** +> Terminal -> Run Task... -> Show All Tasks... -> npm: install -There is a task that will automatically setup your environment to use typescript. +Note: Preserve the `node_modules` folder as it contains necessary dependencies for Typescript and other functionalities. -To run it, you just need to go to: +## **Essential Concepts** -> Terminal->Run Task...->Show All Tasks...->npm: install +Prioritize understanding Dependency Injection and Inversion of Control, the architectural principles SPT-AKI adopts. Comprehensive guidelines will be available on the hub upon release. -After running this task, your environment will be ready to start coding. +Some resources to get you started: + - [A quick intro to Dependency Injection](https://www.freecodecamp.org/news/a-quick-intro-to-dependency-injection-what-it-is-and-when-to-use-it-7578c84fa88f/) + - [Understanding Inversion of Control (IoC) Principle](https://medium.com/@amitkma/understanding-inversion-of-control-ioc-principle-163b1dc97454) -DO NOT remove the node_modules folder, this is an auto generated directory that has the required dependencies to be able to use typescript and more. +## **Coding Guidelines** -## **IMPORTANT:** +Focus your mod development around the `mod.ts` file. In the `package.json` file, only alter these properties: `"name"`, `"version"`, `"license"`, `"author"`, and `"akiVersion"`. -Before starting to work on your mod, we suggest you read about Dependency Injection and Inversion of Control as this is the adopted architecture SPT-AKI has adopted. +New to Typescript? Find comprehensive documentation on the [official website](https://www.typescriptlang.org/docs/). -It will be difficult to understand some of the problems you may be having if you dont understand the basics of it. +## **Distribution Guidelines** -A guide explaining all the essentials will be available on the hub on release for you to read about. +Automated tasks are set up to bundle all necessary files for your mod to function in SPT-AKI: -## **Coding:** +> Terminal -> Run Task... -> Show All Tasks... -> npm: build -All your work should be centered around the mod.ts file as an entry point. -You can ONLY change the following properties from the package.json file: `"name"`, `"version"`, `"license"`: `"MIT"`, `"author"`, `"akiVersion"`. +The ZIP output, located in the `dist` directory, contains all required files. Ensure all files are included and modify the `.buildignore` file as needed. This ZIP file is your uploadable asset for the hub. -If you have never used typescript before, you can read about it here: https://www.typescriptlang.org/docs/ +## **Conclusion** -## **Distributing your mod:** +With this setup, you're ready to begin modding with SPT-AKI. If you run into any trouble be sure to check out the [modding documentation on the hub](https://hub.sp-tarkov.com/doc/lexicon/66-modding/). If you really get stuck feel free to join us in the [#mods-development](https://discord.com/channels/875684761291599922/875803116409323562) official Discord channel. -The project has been set up with an automatic task that will copy and zip ALL required files for your mod to work on SPT-AKI. -To run this task you just need to go to: - -> Terminal->Run Task...->Show All Tasks...->npm: build:zip - -The output will be a mod.zip file that will appear on the root of the project. - -Always verify that all files were included into the zip file. \ No newline at end of file +Build something awesome! diff --git a/TypeScript/12ClassExtensionOverride/build.mjs b/TypeScript/12ClassExtensionOverride/build.mjs new file mode 100644 index 0000000..d00117c --- /dev/null +++ b/TypeScript/12ClassExtensionOverride/build.mjs @@ -0,0 +1,383 @@ +#!/usr/bin/env node + +/** + * Build Script + * + * This script automates the build process for server-side SPT mod projects, facilitating the creation of distributable + * mod packages. It performs a series of operations as outlined below: + * - Loads the .buildignore file, which is used to list files that should be ignored during the build process. + * - Loads the package.json to get project details so a descriptive name can be created for the mod package. + * - Creates a distribution directory and a temporary working directory. + * - Copies files to the temporary directory while respecting the .buildignore rules. + * - Creates a zip archive of the project files. + * - Moves the zip file to the root of the distribution directory. + * - Cleans up the temporary directory. + * + * It's typical that this script be customized to suit the needs of each project. For example, the script can be updated + * to perform additional operations, such as moving the mod package to a specific location or uploading it to a server. + * This script is intended to be a starting point for developers to build upon. + * + * Usage: + * - Run this script using npm: `npm run build` + * - Use `npm run buildinfo` for detailed logging. + * + * Note: + * - Ensure that all necessary Node.js modules are installed before running the script: `npm install` + * - The script reads configurations from the `package.json` and `.buildignore` files; ensure they are correctly set up. + * + * @author Refringe + * @version v1.0.0 + */ + +import fs from "fs-extra"; +import os from "os"; +import path from "path"; +import { fileURLToPath } from "url"; +import { dirname } from "path"; +import ignore from "ignore"; +import archiver from "archiver"; +import winston from "winston"; + +// Get the command line arguments to determine whether to use verbose logging. +const args = process.argv.slice(2); +const verbose = args.includes("--verbose") || args.includes("-v"); + +// Configure the Winston logger to use colours. +const logColors = { + error: "red", + warn: "yellow", + info: "grey", + success: "green", +}; +winston.addColors(logColors); + +// Create a logger instance to log build progress. Configure the logger levels to allow for different levels of logging +// based on the verbosity flag, and set the console transport to log messages of the appropriate level. +const logger = winston.createLogger({ + levels: { + error: 0, + warn: 1, + success: 2, + info: 3, + }, + format: winston.format.combine( + winston.format.colorize(), + winston.format.printf(info => { + return `${info.level}: ${info.message}`; + }) + ), + transports: [ + new winston.transports.Console({ + level: verbose ? "info" : "success", + }), + ], +}); + +/** + * The main function orchestrates the build process for creating a distributable mod package. It leverages a series of + * helper functions to perform various tasks such as loading configuration files, setting up directories, copying files + * according to `.buildignore` rules, and creating a ZIP archive of the project files. + * + * Utilizes the Winston logger to provide information on the build status at different stages of the process. + * + * @returns {void} + */ +async function main() { + // Get the current directory where the script is being executed + const currentDir = getCurrentDirectory(); + + // Defining at this scope because we need to use it in the finally block. + let projectDir; + + try { + // Load the .buildignore file to set up an ignore handler for the build process. + const buildIgnorePatterns = await loadBuildIgnoreFile(currentDir); + + // Load the package.json file to get project details. + const packageJson = await loadPackageJson(currentDir); + + // Create a descriptive name for the mod package. + const projectName = createProjectName(packageJson); + logger.log("success", `Project name created: ${projectName}`); + + // Remove the old distribution directory and create a fresh one. + const distDir = await removeOldDistDirectory(currentDir); + logger.log("info", "Distribution directory successfully cleaned."); + + // Create a temporary working directory to perform the build operations. + projectDir = await createTemporaryDirectoryWithProjectName(projectName); + logger.log("success", "Temporary working directory successfully created."); + logger.log("info", projectDir); + + // Copy files to the temporary directory while respecting the .buildignore rules. + logger.log("info", "Beginning copy operation using .buildignore file..."); + await copyFiles(currentDir, projectDir, buildIgnorePatterns); + logger.log("success", "Files successfully copied to temporary directory."); + + // Create a zip archive of the project files. + logger.log("info", "Beginning folder compression..."); + const zipFilePath = path.join(path.dirname(projectDir), `${projectName}.zip`); + await createZipFile(projectDir, zipFilePath, "user/mods/" + projectName); + logger.log("success", "Archive successfully created."); + logger.log("info", zipFilePath); + + // Move the zip file inside of the project directory, within the temporary working directory. + const zipFileInProjectDir = path.join(projectDir, `${projectName}.zip`); + await fs.move(zipFilePath, zipFileInProjectDir); + logger.log("success", "Archive successfully moved."); + logger.log("info", zipFileInProjectDir); + + // Move the temporary directory into the distribution directory. + await fs.move(projectDir, distDir); + logger.log("success", "Temporary directory successfully moved into project distribution directory."); + + // Log the success message. Write out the path to the mod package. + logger.log("success", "------------------------------------"); + logger.log("success", "Build script completed successfully!"); + logger.log("success", "Your mod package has been created in the 'dist' directory:"); + logger.log("success", `/${path.relative(process.cwd(), path.join(distDir, `${projectName}.zip`))}`); + logger.log("success", "------------------------------------"); + if (!verbose) { + logger.log("success", "To see a detailed build log, use `npm run buildinfo`."); + logger.log("success", "------------------------------------"); + } + } catch (err) { + // If any of the file operations fail, log the error. + logger.log("error", "An error occurred: " + err); + } finally { + // Clean up the temporary directory, even if the build fails. + if (projectDir) { + try { + await fs.promises.rm(projectDir, { force: true, recursive: true }); + logger.log("info", "Cleaned temporary directory."); + } catch (err) { + logger.log("error", "Failed to clean temporary directory: " + err); + } + } + } +} + +/** + * Retrieves the current working directory where the script is being executed. This directory is used as a reference + * point for various file operations throughout the build process, ensuring that paths are resolved correctly regardless + * of the location from which the script is invoked. + * + * @returns {string} The absolute path of the current working directory. + */ +function getCurrentDirectory() { + return dirname(fileURLToPath(import.meta.url)); +} + +/** + * Loads the `.buildignore` file and sets up an ignore handler using the `ignore` module. The `.buildignore` file + * contains a list of patterns describing files and directories that should be ignored during the build process. The + * ignore handler created by this method is used to filter files and directories when copying them to the temporary + * directory, ensuring that only necessary files are included in the final mod package. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to an ignore handler. + */ +async function loadBuildIgnoreFile(currentDir) { + const buildIgnorePath = path.join(currentDir, ".buildignore"); + + try { + // Attempt to read the contents of the .buildignore file asynchronously. + const fileContent = await fs.promises.readFile(buildIgnorePath, "utf-8"); + + // Return a new ignore instance and add the rules from the .buildignore file (split by newlines). + return ignore().add(fileContent.split("\n")); + } catch (err) { + logger.log("warn", "Failed to read .buildignore file. No files or directories will be ignored."); + + // Return an empty ignore instance, ensuring the build process can continue. + return ignore(); + } +} + +/** + * Loads the `package.json` file and returns its content as a JSON object. The `package.json` file contains important + * project details such as the name and version, which are used in later stages of the build process to create a + * descriptive name for the mod package. The method reads the file from the current working directory, ensuring that it + * accurately reflects the current state of the project. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to a JSON object containing the contents of the `package.json`. + */ +async function loadPackageJson(currentDir) { + const packageJsonPath = path.join(currentDir, "package.json"); + + // Read the contents of the package.json file asynchronously as a UTF-8 string. + const packageJsonContent = await fs.promises.readFile(packageJsonPath, "utf-8"); + + return JSON.parse(packageJsonContent); +} + +/** + * Constructs a descriptive name for the mod package using details from the `package.json` file. The name is created by + * concatenating the project name, version, and a timestamp, resulting in a unique and descriptive file name for each + * build. This name is used as the base name for the temporary working directory and the final ZIP archive, helping to + * identify different versions of the mod package easily. + * + * @param {Object} packageJson - A JSON object containing the contents of the `package.json` file. + * @returns {string} A string representing the constructed project name. + */ +function createProjectName(packageJson) { + // Remove any non-alphanumeric characters from the author and name. + const author = packageJson.author.replace(/\W/g, ""); + const name = packageJson.name.replace(/\W/g, ""); + const version = packageJson.version; + + // Ensure the name is lowercase, as per the package.json specification. + return `${author}-${name}-${version}`.toLowerCase(); +} + +/** + * Defines the location of the distribution directory where the final mod package will be stored and deletes any + * existing distribution directory to ensure a clean slate for the build process. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to the absolute path to the distribution directory. + */ +async function removeOldDistDirectory(projectDir) { + const distPath = path.join(projectDir, "dist"); + await fs.remove(distPath); + return distPath; +} + +/** + * Creates a temporary working directory using the project name. This directory serves as a staging area where project + * files are gathered before being archived into the final mod package. The method constructs a unique directory path + * by appending the project name to a base temporary directory path, ensuring that each build has its own isolated + * working space. This approach facilitates clean and organized build processes, avoiding potential conflicts with other + * builds. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @param {string} projectName - The constructed project name, used to create a unique path for the temporary directory. + * @returns {Promise} A promise that resolves to the absolute path of the newly created temporary directory. + */ +async function createTemporaryDirectoryWithProjectName(projectName) { + // Create a new directory in the system's temporary folder to hold the project files. + const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "spt-mod-build-")); + + // Create a subdirectory within the temporary directory using the project name for this specific build. + const projectDir = path.join(tempDir, projectName); + await fs.ensureDir(projectDir); + + return projectDir; +} + +/** + * Copies the project files to the temporary directory while respecting the rules defined in the `.buildignore` file. + * The method is recursive, iterating over all files and directories in the source directory and using the ignore + * handler to filter out files and directories that match the patterns defined in the `.buildignore` file. This ensures + * that only the necessary files are included in the final mod package, adhering to the specifications defined by the + * developer in the `.buildignore` file. + * + * The copy operations are delayed and executed in parallel to improve efficiency and reduce the build time. This is + * achieved by creating an array of copy promises and awaiting them all at the end of the function. + * + * @param {string} sourceDirectory - The absolute path of the current working directory. + * @param {string} destinationDirectory - The absolute path of the temporary directory where the files will be copied. + * @param {Ignore} ignoreHandler - The ignore handler created from the `.buildignore` file. + * @returns {Promise} A promise that resolves when all copy operations are completed successfully. + */ +async function copyFiles(srcDir, destDir, ignoreHandler) { + try { + // Read the contents of the source directory to get a list of entries (files and directories). + const entries = await fs.promises.readdir(srcDir, { withFileTypes: true }); + + // Initialize an array to hold the promises returned by recursive calls to copyFiles and copyFile operations. + const copyOperations = []; + + for (const entry of entries) { + // Define the source and destination paths for each entry. + const srcPath = path.join(srcDir, entry.name); + const destPath = path.join(destDir, entry.name); + + // Get the relative path of the source file to check against the ignore handler. + const relativePath = path.relative(process.cwd(), srcPath); + + // If the ignore handler dictates that this file should be ignored, skip to the next iteration. + if (ignoreHandler.ignores(relativePath)) { + logger.log("info", `Ignored: /${path.relative(process.cwd(), srcPath)}`); + continue; + } + + if (entry.isDirectory()) { + // If the entry is a directory, create the corresponding temporary directory and make a recursive call + // to copyFiles to handle copying the contents of the directory. + await fs.ensureDir(destPath); + copyOperations.push(copyFiles(srcPath, destPath, ignoreHandler)); + } else { + // If the entry is a file, add a copyFile operation to the copyOperations array and log the event when + // the operation is successful. + copyOperations.push( + fs.copy(srcPath, destPath).then(() => { + logger.log("info", `Copied: /${path.relative(process.cwd(), srcPath)}`); + }) + ); + } + } + + // Await all copy operations to ensure all files and directories are copied before exiting the function. + await Promise.all(copyOperations); + } catch (err) { + // Log an error message if any error occurs during the copy process. + logger.log("error", "Error copying files: " + err); + } +} + +/** + * Creates a ZIP archive of the project files located in the temporary directory. The method uses the `archiver` module + * to create a ZIP file, which includes all the files that have been copied to the temporary directory during the build + * process. The ZIP file is named using the project name, helping to identify the contents of the archive easily. + * + * @param {string} directoryPath - The absolute path of the temporary directory containing the project files. + * @param {string} projectName - The constructed project name, used to name the ZIP file. + * @returns {Promise} A promise that resolves to the absolute path of the created ZIP file. + */ +async function createZipFile(directoryToZip, zipFilePath, containerDirName) { + return new Promise((resolve, reject) => { + // Create a write stream to the specified ZIP file path. + const output = fs.createWriteStream(zipFilePath); + + // Create a new archiver instance with ZIP format and maximum compression level. + const archive = archiver("zip", { + zlib: { level: 9 }, + }); + + // Set up an event listener for the 'close' event to resolve the promise when the archiver has finalized. + output.on("close", function () { + logger.log("info", "Archiver has finalized. The output and the file descriptor have closed."); + resolve(); + }); + + // Set up an event listener for the 'warning' event to handle warnings appropriately, logging them or rejecting + // the promise based on the error code. + archive.on("warning", function (err) { + if (err.code === "ENOENT") { + logger.log("warn", `Archiver issued a warning: ${err.code} - ${err.message}`); + } else { + reject(err); + } + }); + + // Set up an event listener for the 'error' event to reject the promise if any error occurs during archiving. + archive.on("error", function (err) { + reject(err); + }); + + // Pipe archive data to the file. + archive.pipe(output); + + // Add the directory to the archive, under the provided directory name. + archive.directory(directoryToZip, containerDirName); + + // Finalize the archive, indicating that no more files will be added and triggering the 'close' event once all + // data has been written. + archive.finalize(); + }); +} + +// Engage! +main(); diff --git a/TypeScript/12ClassExtensionOverride/package.json b/TypeScript/12ClassExtensionOverride/package.json index adfd723..5a12315 100644 --- a/TypeScript/12ClassExtensionOverride/package.json +++ b/TypeScript/12ClassExtensionOverride/package.json @@ -7,17 +7,20 @@ "akiVersion": "~3.7", "scripts": { "setup": "npm i", - "build": "node ./packageBuild.ts" + "build": "node ./build.mjs", + "buildinfo": "node ./build.mjs --verbose" }, "devDependencies": { "@types/node": "20.4.5", "@typescript-eslint/eslint-plugin": "6.2.0", "@typescript-eslint/parser": "6.2.0", - "bestzip": "2.2.1", + "archiver": "^6.0", "eslint": "8.46.0", - "fs-extra": "11.1.1", - "glob": "10.3.3", + "fs-extra": "^11.1", + "ignore": "^5.2", + "os": "^0.1", "tsyringe": "4.8.0", - "typescript": "5.1.6" + "typescript": "5.1.6", + "winston": "^3.9" } -} \ No newline at end of file +} diff --git a/TypeScript/12ClassExtensionOverride/packageBuild.ts b/TypeScript/12ClassExtensionOverride/packageBuild.ts deleted file mode 100644 index d76ddd8..0000000 --- a/TypeScript/12ClassExtensionOverride/packageBuild.ts +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env node - -// This is a simple script used to build a mod package. The script will copy necessary files to the build directory -// and compress the build directory into a zip file that can be easily shared. - -const fs = require("fs-extra"); -const glob = require("glob"); -const zip = require('bestzip'); -const path = require("path"); -const minimatch = require('minimatch'); - -// Load the package.json file to get some information about the package so we can name things appropriately. This is -// atypical, and you would never do this in a production environment, but this script is only used for development so -// it's fine in this case. Some of these values are stored in environment variables, but those differ between node -// versions; the 'author' value is not available after node v14. -const { author, name:packageName, version } = require("./package.json"); - -// Generate the name of the package, stripping out all non-alphanumeric characters in the 'author' and 'name'. -const modName = `${author.replace(/[^a-z0-9]/gi, "")}-${packageName.replace(/[^a-z0-9]/gi, "")}-${version}`; -console.log(`Generated package name: ${modName}`); - -// Delete the old build directory and compressed package file. -fs.rmSync(`${__dirname}/dist`, { force: true, recursive: true }); -console.log("Previous build files deleted."); - -// Generate a list of files that should not be copied over into the distribution directory. This is a blacklist to ensure -// we always copy over additional files and directories that authors may have added to their project. This may need to be -// expanded upon by the mod author to allow for node modules that are used within the mod; example commented out below. -const ignoreList = [ - "node_modules/", - // "node_modules/!(weighted|glob)", // Instead of excluding the entire node_modules directory, allow two node modules. - "src/**/*.js", - "types/", - ".git/", - ".gitea/", - ".eslintignore", - ".eslintrc.json", - ".gitignore", - ".DS_Store", - "packageBuild.ts", - "mod.code-workspace", - "package-lock.json", - "tsconfig.json" -]; -const exclude = glob.sync(`{${ignoreList.join(",")}}`, { realpath: true, dot: true }); - -fs.copySync(__dirname, path.normalize(`${__dirname}/../~${modName}`), {filter: (src) => -{ - const relativePath = path.relative(__dirname, src); - const shouldExclude = exclude.some((pattern) => minimatch(relativePath, pattern)); - console.log(`${relativePath} - Excluded: ${shouldExclude}`); - return !shouldExclude; -},}); - -fs.moveSync(path.normalize(`${__dirname}/../~${modName}`), path.normalize(`${__dirname}/${modName}`), { overwrite: true }); -fs.copySync(path.normalize(`${__dirname}/${modName}`), path.normalize(`${__dirname}/dist`)); -console.log("Build files copied."); - -// Compress the files for easy distribution. The compressed file is saved into the dist directory. When uncompressed we -// need to be sure that it includes a directory that the user can easily copy into their game mods directory. -zip({ - source: modName, - destination: `dist/${modName}.zip`, - cwd: __dirname -}).catch(function(err) -{ - console.error("A bestzip error has occurred: ", err.stack); -}).then(function() -{ - console.log(`Compressed mod package to: /dist/${modName}.zip`); - - // Now that we're done with the compression we can delete the temporary build directory. - fs.rmSync(`${__dirname}/${modName}`, { force: true, recursive: true }); - console.log("Build successful! your zip file has been created and is ready to be uploaded to hub.sp-tarkov.com/files/"); -}); \ No newline at end of file diff --git a/TypeScript/13AddTrader/.buildignore b/TypeScript/13AddTrader/.buildignore new file mode 100644 index 0000000..2cbde65 --- /dev/null +++ b/TypeScript/13AddTrader/.buildignore @@ -0,0 +1,20 @@ +/.buildignore +/.DS_Store +/.editorconfig +/.eslintignore +/.eslintrc.json +/.git +/.github +/.gitignore +/.gitlab +/.nvmrc +/.prettierrc +/.vscode +/build.mjs +/dist +/images +/mod.code-workspace +/node_modules +/package-lock.json +/tsconfig.json +/types diff --git a/TypeScript/13AddTrader/.eslintrc.json b/TypeScript/13AddTrader/.eslintrc.json index c505160..071a313 100644 --- a/TypeScript/13AddTrader/.eslintrc.json +++ b/TypeScript/13AddTrader/.eslintrc.json @@ -1,75 +1,98 @@ { - "root": true, - "parser": "@typescript-eslint/parser", - "plugins": [ - "@typescript-eslint" - ], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended" - ], - "rules": { - "@typescript-eslint/no-explicit-any": 0, - "@typescript-eslint/no-unused-vars": 1, - "@typescript-eslint/no-empty-interface": 0, - "@typescript-eslint/no-namespace": 0, - "@typescript-eslint/comma-dangle": 1, - "@typescript-eslint/func-call-spacing": 2, - "@typescript-eslint/quotes": 1, - "@typescript-eslint/brace-style": [ - "warn", - "allman" + "root": true, + "parser": "@typescript-eslint/parser", + "plugins": [ + "@typescript-eslint" ], - "@typescript-eslint/naming-convention": [ - "warn", - { - "selector": "default", - "format": ["camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "typeLike", - "format": ["PascalCase"] - }, - { - "selector": "objectLiteralProperty", - "format": ["PascalCase", "camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "typeProperty", - "format": ["PascalCase", "camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "enumMember", - "format": ["UPPER_CASE"] - } + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" ], - "@typescript-eslint/indent": [ - "warn", - 4 - ], - "@typescript-eslint/no-unused-expressions": [ - "warn", - { - "allowShortCircuit": false, - "allowTernary": false - } - ], - "@typescript-eslint/keyword-spacing": [ - "warn", - { - "before": true, - "after": true - } - ], - "@typescript-eslint/explicit-module-boundary-types": [ - "warn", - { - "allowArgumentsExplicitlyTypedAsAny": true - } + "rules": { + "@typescript-eslint/no-explicit-any": 0, + "@typescript-eslint/no-unused-vars": 1, + "@typescript-eslint/no-empty-interface": 0, + "@typescript-eslint/no-namespace": 0, + "@typescript-eslint/comma-dangle": 1, + "@typescript-eslint/func-call-spacing": 2, + "@typescript-eslint/quotes": 1, + "@typescript-eslint/brace-style": [ + "warn", + "allman" + ], + "@typescript-eslint/naming-convention": [ + "warn", + { + "selector": "default", + "format": [ + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "typeLike", + "format": [ + "PascalCase" + ] + }, + { + "selector": "objectLiteralProperty", + "format": [ + "PascalCase", + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "typeProperty", + "format": [ + "PascalCase", + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "enumMember", + "format": [ + "UPPER_CASE" + ] + } + ], + "@typescript-eslint/indent": [ + "warn", + 4 + ], + "@typescript-eslint/no-unused-expressions": [ + "warn", + { + "allowShortCircuit": false, + "allowTernary": false + } + ], + "@typescript-eslint/keyword-spacing": [ + "warn", + { + "before": true, + "after": true + } + ], + "@typescript-eslint/explicit-module-boundary-types": [ + "warn", + { + "allowArgumentsExplicitlyTypedAsAny": true + } + ] + }, + "overrides": [ + { + "files": [ + "*.mjs", + "*.ts" + ], + "env": { + "node": true + } + } ] - } } \ No newline at end of file diff --git a/TypeScript/13AddTrader/README.md b/TypeScript/13AddTrader/README.md index 434d063..8cba28e 100644 --- a/TypeScript/13AddTrader/README.md +++ b/TypeScript/13AddTrader/README.md @@ -1,64 +1,66 @@ +# Welcome to the SPT-AKI Modding Project -This project was created to automate most parts of building and setting up an environment. +This project is designed to streamline the initial setup process for building and creating mods in the SPT-AKI environment. Follow this guide to set up your environment efficiently. -## **NodeJS:** +## **Table of Contents** +- [NodeJS Setup](#nodejs-setup) +- [IDE Setup](#ide-setup) +- [Workspace Configuration](#workspace-configuration) +- [Environment Setup](#environment-setup) +- [Essential Concepts](#essential-concepts) +- [Coding Guidelines](#coding-guidelines) +- [Distribution Guidelines](#distribution-guidelines) -The first step would be to install nodejs on your pc, the version you NEED is **16.17.1** +## **NodeJS Setup** -That version is the one that has been used to test the mod templates and build scripts. +Before you begin, ensure to install NodeJS version `v16.17.1`, which has been tested thoroughly with our mod templates and build scripts. Download it from the [official NodeJS website](https://nodejs.org/). -It can be downloaded from here: https://nodejs.org/dist/v16.17.1/node-v16.17.1-x64.msi +After installation, it's advised to reboot your system. -A system reboot may be needed after install. +## **IDE Setup** -## **IDE:** +For this project, you can work with either [VSCodium](https://vscodium.com/) or [VSCode](https://code.visualstudio.com/). However, we strongly recommend using VSCode, as all development and testing have been carried out using this IDE, ensuring a smoother experience and compatibility with the project setups. Either way, we have a prepared a workspace file to assist you in setting up your environment. -The second step is having an IDE ready. We've setup a VSCodium workspace file to help with this. +## **Workspace Configuration** -You CAN use Visual Studio Code if you so desire, just keep in mind that our devs tests on the mod files was done using VSCode. +With NodeJS and your chosen IDE ready, initiate the `mod.code-workspace` file using your IDE: -You can get VSCode here: https://code.visualstudio.com/ +> File -> Open Workspace from File... -## **Workspace:** +Upon project loading, consider installing recommended plugins like the ESLint plugin. -Once you have NodeJS and VSCode/VSCodium ready, open the mod.code-workspace file with VSCode (File->Open Workspace from File...). +## **Environment Setup** -Once the project loads you may be recommended to install the ESLint plugin. This is HIGHLY recommended. +An automated task is available to configure your environment for Typescript utilization: -## **Environment Setup:** +> Terminal -> Run Task... -> Show All Tasks... -> npm: install -There is a task that will automatically setup your environment to use typescript. +Note: Preserve the `node_modules` folder as it contains necessary dependencies for Typescript and other functionalities. -To run it, you just need to go to: +## **Essential Concepts** -> Terminal->Run Task...->Show All Tasks...->npm: install +Prioritize understanding Dependency Injection and Inversion of Control, the architectural principles SPT-AKI adopts. Comprehensive guidelines will be available on the hub upon release. -After running this task, your environment will be ready to start coding. +Some resources to get you started: + - [A quick intro to Dependency Injection](https://www.freecodecamp.org/news/a-quick-intro-to-dependency-injection-what-it-is-and-when-to-use-it-7578c84fa88f/) + - [Understanding Inversion of Control (IoC) Principle](https://medium.com/@amitkma/understanding-inversion-of-control-ioc-principle-163b1dc97454) -DO NOT remove the node_modules folder, this is an auto generated directory that has the required dependencies to be able to use typescript and more. +## **Coding Guidelines** -## **IMPORTANT:** +Focus your mod development around the `mod.ts` file. In the `package.json` file, only alter these properties: `"name"`, `"version"`, `"license"`, `"author"`, and `"akiVersion"`. -Before starting to work on your mod, we suggest you read about Dependency Injection and Inversion of Control as this is the adopted architecture SPT-AKI has adopted. +New to Typescript? Find comprehensive documentation on the [official website](https://www.typescriptlang.org/docs/). -It will be difficult to understand some of the problems you may be having if you dont understand the basics of it. +## **Distribution Guidelines** -A guide explaining all the essentials will be available on the hub on release for you to read about. +Automated tasks are set up to bundle all necessary files for your mod to function in SPT-AKI: -## **Coding:** +> Terminal -> Run Task... -> Show All Tasks... -> npm: build -All your work should be centered around the mod.ts file as an entry point. -You can ONLY change the following properties from the package.json file: `"name"`, `"version"`, `"license"`: `"MIT"`, `"author"`, `"akiVersion"`. +The ZIP output, located in the `dist` directory, contains all required files. Ensure all files are included and modify the `.buildignore` file as needed. This ZIP file is your uploadable asset for the hub. -If you have never used typescript before, you can read about it here: https://www.typescriptlang.org/docs/ +## **Conclusion** -## **Distributing your mod:** +With this setup, you're ready to begin modding with SPT-AKI. If you run into any trouble be sure to check out the [modding documentation on the hub](https://hub.sp-tarkov.com/doc/lexicon/66-modding/). If you really get stuck feel free to join us in the [#mods-development](https://discord.com/channels/875684761291599922/875803116409323562) official Discord channel. -The project has been set up with an automatic task that will copy and zip ALL required files for your mod to work on SPT-AKI. -To run this task you just need to go to: - -> Terminal->Run Task...->Show All Tasks...->npm: build:zip - -The output will be a mod.zip file that will appear on the root of the project. - -Always verify that all files were included into the zip file. \ No newline at end of file +Build something awesome! diff --git a/TypeScript/13AddTrader/build.mjs b/TypeScript/13AddTrader/build.mjs new file mode 100644 index 0000000..d00117c --- /dev/null +++ b/TypeScript/13AddTrader/build.mjs @@ -0,0 +1,383 @@ +#!/usr/bin/env node + +/** + * Build Script + * + * This script automates the build process for server-side SPT mod projects, facilitating the creation of distributable + * mod packages. It performs a series of operations as outlined below: + * - Loads the .buildignore file, which is used to list files that should be ignored during the build process. + * - Loads the package.json to get project details so a descriptive name can be created for the mod package. + * - Creates a distribution directory and a temporary working directory. + * - Copies files to the temporary directory while respecting the .buildignore rules. + * - Creates a zip archive of the project files. + * - Moves the zip file to the root of the distribution directory. + * - Cleans up the temporary directory. + * + * It's typical that this script be customized to suit the needs of each project. For example, the script can be updated + * to perform additional operations, such as moving the mod package to a specific location or uploading it to a server. + * This script is intended to be a starting point for developers to build upon. + * + * Usage: + * - Run this script using npm: `npm run build` + * - Use `npm run buildinfo` for detailed logging. + * + * Note: + * - Ensure that all necessary Node.js modules are installed before running the script: `npm install` + * - The script reads configurations from the `package.json` and `.buildignore` files; ensure they are correctly set up. + * + * @author Refringe + * @version v1.0.0 + */ + +import fs from "fs-extra"; +import os from "os"; +import path from "path"; +import { fileURLToPath } from "url"; +import { dirname } from "path"; +import ignore from "ignore"; +import archiver from "archiver"; +import winston from "winston"; + +// Get the command line arguments to determine whether to use verbose logging. +const args = process.argv.slice(2); +const verbose = args.includes("--verbose") || args.includes("-v"); + +// Configure the Winston logger to use colours. +const logColors = { + error: "red", + warn: "yellow", + info: "grey", + success: "green", +}; +winston.addColors(logColors); + +// Create a logger instance to log build progress. Configure the logger levels to allow for different levels of logging +// based on the verbosity flag, and set the console transport to log messages of the appropriate level. +const logger = winston.createLogger({ + levels: { + error: 0, + warn: 1, + success: 2, + info: 3, + }, + format: winston.format.combine( + winston.format.colorize(), + winston.format.printf(info => { + return `${info.level}: ${info.message}`; + }) + ), + transports: [ + new winston.transports.Console({ + level: verbose ? "info" : "success", + }), + ], +}); + +/** + * The main function orchestrates the build process for creating a distributable mod package. It leverages a series of + * helper functions to perform various tasks such as loading configuration files, setting up directories, copying files + * according to `.buildignore` rules, and creating a ZIP archive of the project files. + * + * Utilizes the Winston logger to provide information on the build status at different stages of the process. + * + * @returns {void} + */ +async function main() { + // Get the current directory where the script is being executed + const currentDir = getCurrentDirectory(); + + // Defining at this scope because we need to use it in the finally block. + let projectDir; + + try { + // Load the .buildignore file to set up an ignore handler for the build process. + const buildIgnorePatterns = await loadBuildIgnoreFile(currentDir); + + // Load the package.json file to get project details. + const packageJson = await loadPackageJson(currentDir); + + // Create a descriptive name for the mod package. + const projectName = createProjectName(packageJson); + logger.log("success", `Project name created: ${projectName}`); + + // Remove the old distribution directory and create a fresh one. + const distDir = await removeOldDistDirectory(currentDir); + logger.log("info", "Distribution directory successfully cleaned."); + + // Create a temporary working directory to perform the build operations. + projectDir = await createTemporaryDirectoryWithProjectName(projectName); + logger.log("success", "Temporary working directory successfully created."); + logger.log("info", projectDir); + + // Copy files to the temporary directory while respecting the .buildignore rules. + logger.log("info", "Beginning copy operation using .buildignore file..."); + await copyFiles(currentDir, projectDir, buildIgnorePatterns); + logger.log("success", "Files successfully copied to temporary directory."); + + // Create a zip archive of the project files. + logger.log("info", "Beginning folder compression..."); + const zipFilePath = path.join(path.dirname(projectDir), `${projectName}.zip`); + await createZipFile(projectDir, zipFilePath, "user/mods/" + projectName); + logger.log("success", "Archive successfully created."); + logger.log("info", zipFilePath); + + // Move the zip file inside of the project directory, within the temporary working directory. + const zipFileInProjectDir = path.join(projectDir, `${projectName}.zip`); + await fs.move(zipFilePath, zipFileInProjectDir); + logger.log("success", "Archive successfully moved."); + logger.log("info", zipFileInProjectDir); + + // Move the temporary directory into the distribution directory. + await fs.move(projectDir, distDir); + logger.log("success", "Temporary directory successfully moved into project distribution directory."); + + // Log the success message. Write out the path to the mod package. + logger.log("success", "------------------------------------"); + logger.log("success", "Build script completed successfully!"); + logger.log("success", "Your mod package has been created in the 'dist' directory:"); + logger.log("success", `/${path.relative(process.cwd(), path.join(distDir, `${projectName}.zip`))}`); + logger.log("success", "------------------------------------"); + if (!verbose) { + logger.log("success", "To see a detailed build log, use `npm run buildinfo`."); + logger.log("success", "------------------------------------"); + } + } catch (err) { + // If any of the file operations fail, log the error. + logger.log("error", "An error occurred: " + err); + } finally { + // Clean up the temporary directory, even if the build fails. + if (projectDir) { + try { + await fs.promises.rm(projectDir, { force: true, recursive: true }); + logger.log("info", "Cleaned temporary directory."); + } catch (err) { + logger.log("error", "Failed to clean temporary directory: " + err); + } + } + } +} + +/** + * Retrieves the current working directory where the script is being executed. This directory is used as a reference + * point for various file operations throughout the build process, ensuring that paths are resolved correctly regardless + * of the location from which the script is invoked. + * + * @returns {string} The absolute path of the current working directory. + */ +function getCurrentDirectory() { + return dirname(fileURLToPath(import.meta.url)); +} + +/** + * Loads the `.buildignore` file and sets up an ignore handler using the `ignore` module. The `.buildignore` file + * contains a list of patterns describing files and directories that should be ignored during the build process. The + * ignore handler created by this method is used to filter files and directories when copying them to the temporary + * directory, ensuring that only necessary files are included in the final mod package. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to an ignore handler. + */ +async function loadBuildIgnoreFile(currentDir) { + const buildIgnorePath = path.join(currentDir, ".buildignore"); + + try { + // Attempt to read the contents of the .buildignore file asynchronously. + const fileContent = await fs.promises.readFile(buildIgnorePath, "utf-8"); + + // Return a new ignore instance and add the rules from the .buildignore file (split by newlines). + return ignore().add(fileContent.split("\n")); + } catch (err) { + logger.log("warn", "Failed to read .buildignore file. No files or directories will be ignored."); + + // Return an empty ignore instance, ensuring the build process can continue. + return ignore(); + } +} + +/** + * Loads the `package.json` file and returns its content as a JSON object. The `package.json` file contains important + * project details such as the name and version, which are used in later stages of the build process to create a + * descriptive name for the mod package. The method reads the file from the current working directory, ensuring that it + * accurately reflects the current state of the project. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to a JSON object containing the contents of the `package.json`. + */ +async function loadPackageJson(currentDir) { + const packageJsonPath = path.join(currentDir, "package.json"); + + // Read the contents of the package.json file asynchronously as a UTF-8 string. + const packageJsonContent = await fs.promises.readFile(packageJsonPath, "utf-8"); + + return JSON.parse(packageJsonContent); +} + +/** + * Constructs a descriptive name for the mod package using details from the `package.json` file. The name is created by + * concatenating the project name, version, and a timestamp, resulting in a unique and descriptive file name for each + * build. This name is used as the base name for the temporary working directory and the final ZIP archive, helping to + * identify different versions of the mod package easily. + * + * @param {Object} packageJson - A JSON object containing the contents of the `package.json` file. + * @returns {string} A string representing the constructed project name. + */ +function createProjectName(packageJson) { + // Remove any non-alphanumeric characters from the author and name. + const author = packageJson.author.replace(/\W/g, ""); + const name = packageJson.name.replace(/\W/g, ""); + const version = packageJson.version; + + // Ensure the name is lowercase, as per the package.json specification. + return `${author}-${name}-${version}`.toLowerCase(); +} + +/** + * Defines the location of the distribution directory where the final mod package will be stored and deletes any + * existing distribution directory to ensure a clean slate for the build process. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to the absolute path to the distribution directory. + */ +async function removeOldDistDirectory(projectDir) { + const distPath = path.join(projectDir, "dist"); + await fs.remove(distPath); + return distPath; +} + +/** + * Creates a temporary working directory using the project name. This directory serves as a staging area where project + * files are gathered before being archived into the final mod package. The method constructs a unique directory path + * by appending the project name to a base temporary directory path, ensuring that each build has its own isolated + * working space. This approach facilitates clean and organized build processes, avoiding potential conflicts with other + * builds. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @param {string} projectName - The constructed project name, used to create a unique path for the temporary directory. + * @returns {Promise} A promise that resolves to the absolute path of the newly created temporary directory. + */ +async function createTemporaryDirectoryWithProjectName(projectName) { + // Create a new directory in the system's temporary folder to hold the project files. + const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "spt-mod-build-")); + + // Create a subdirectory within the temporary directory using the project name for this specific build. + const projectDir = path.join(tempDir, projectName); + await fs.ensureDir(projectDir); + + return projectDir; +} + +/** + * Copies the project files to the temporary directory while respecting the rules defined in the `.buildignore` file. + * The method is recursive, iterating over all files and directories in the source directory and using the ignore + * handler to filter out files and directories that match the patterns defined in the `.buildignore` file. This ensures + * that only the necessary files are included in the final mod package, adhering to the specifications defined by the + * developer in the `.buildignore` file. + * + * The copy operations are delayed and executed in parallel to improve efficiency and reduce the build time. This is + * achieved by creating an array of copy promises and awaiting them all at the end of the function. + * + * @param {string} sourceDirectory - The absolute path of the current working directory. + * @param {string} destinationDirectory - The absolute path of the temporary directory where the files will be copied. + * @param {Ignore} ignoreHandler - The ignore handler created from the `.buildignore` file. + * @returns {Promise} A promise that resolves when all copy operations are completed successfully. + */ +async function copyFiles(srcDir, destDir, ignoreHandler) { + try { + // Read the contents of the source directory to get a list of entries (files and directories). + const entries = await fs.promises.readdir(srcDir, { withFileTypes: true }); + + // Initialize an array to hold the promises returned by recursive calls to copyFiles and copyFile operations. + const copyOperations = []; + + for (const entry of entries) { + // Define the source and destination paths for each entry. + const srcPath = path.join(srcDir, entry.name); + const destPath = path.join(destDir, entry.name); + + // Get the relative path of the source file to check against the ignore handler. + const relativePath = path.relative(process.cwd(), srcPath); + + // If the ignore handler dictates that this file should be ignored, skip to the next iteration. + if (ignoreHandler.ignores(relativePath)) { + logger.log("info", `Ignored: /${path.relative(process.cwd(), srcPath)}`); + continue; + } + + if (entry.isDirectory()) { + // If the entry is a directory, create the corresponding temporary directory and make a recursive call + // to copyFiles to handle copying the contents of the directory. + await fs.ensureDir(destPath); + copyOperations.push(copyFiles(srcPath, destPath, ignoreHandler)); + } else { + // If the entry is a file, add a copyFile operation to the copyOperations array and log the event when + // the operation is successful. + copyOperations.push( + fs.copy(srcPath, destPath).then(() => { + logger.log("info", `Copied: /${path.relative(process.cwd(), srcPath)}`); + }) + ); + } + } + + // Await all copy operations to ensure all files and directories are copied before exiting the function. + await Promise.all(copyOperations); + } catch (err) { + // Log an error message if any error occurs during the copy process. + logger.log("error", "Error copying files: " + err); + } +} + +/** + * Creates a ZIP archive of the project files located in the temporary directory. The method uses the `archiver` module + * to create a ZIP file, which includes all the files that have been copied to the temporary directory during the build + * process. The ZIP file is named using the project name, helping to identify the contents of the archive easily. + * + * @param {string} directoryPath - The absolute path of the temporary directory containing the project files. + * @param {string} projectName - The constructed project name, used to name the ZIP file. + * @returns {Promise} A promise that resolves to the absolute path of the created ZIP file. + */ +async function createZipFile(directoryToZip, zipFilePath, containerDirName) { + return new Promise((resolve, reject) => { + // Create a write stream to the specified ZIP file path. + const output = fs.createWriteStream(zipFilePath); + + // Create a new archiver instance with ZIP format and maximum compression level. + const archive = archiver("zip", { + zlib: { level: 9 }, + }); + + // Set up an event listener for the 'close' event to resolve the promise when the archiver has finalized. + output.on("close", function () { + logger.log("info", "Archiver has finalized. The output and the file descriptor have closed."); + resolve(); + }); + + // Set up an event listener for the 'warning' event to handle warnings appropriately, logging them or rejecting + // the promise based on the error code. + archive.on("warning", function (err) { + if (err.code === "ENOENT") { + logger.log("warn", `Archiver issued a warning: ${err.code} - ${err.message}`); + } else { + reject(err); + } + }); + + // Set up an event listener for the 'error' event to reject the promise if any error occurs during archiving. + archive.on("error", function (err) { + reject(err); + }); + + // Pipe archive data to the file. + archive.pipe(output); + + // Add the directory to the archive, under the provided directory name. + archive.directory(directoryToZip, containerDirName); + + // Finalize the archive, indicating that no more files will be added and triggering the 'close' event once all + // data has been written. + archive.finalize(); + }); +} + +// Engage! +main(); diff --git a/TypeScript/13AddTrader/package.json b/TypeScript/13AddTrader/package.json index 50da204..3ddfb90 100644 --- a/TypeScript/13AddTrader/package.json +++ b/TypeScript/13AddTrader/package.json @@ -7,17 +7,20 @@ "akiVersion": "~3.7", "scripts": { "setup": "npm i", - "build": "node ./packageBuild.ts" + "build": "node ./build.mjs", + "buildinfo": "node ./build.mjs --verbose" }, "devDependencies": { "@types/node": "20.4.5", "@typescript-eslint/eslint-plugin": "6.2.0", "@typescript-eslint/parser": "6.2.0", - "bestzip": "2.2.1", + "archiver": "^6.0", "eslint": "8.46.0", - "fs-extra": "11.1.1", - "glob": "10.3.3", + "fs-extra": "^11.1", + "ignore": "^5.2", + "os": "^0.1", "tsyringe": "4.8.0", - "typescript": "5.1.6" + "typescript": "5.1.6", + "winston": "^3.9" } -} \ No newline at end of file +} diff --git a/TypeScript/13AddTrader/packageBuild.ts b/TypeScript/13AddTrader/packageBuild.ts deleted file mode 100644 index d76ddd8..0000000 --- a/TypeScript/13AddTrader/packageBuild.ts +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env node - -// This is a simple script used to build a mod package. The script will copy necessary files to the build directory -// and compress the build directory into a zip file that can be easily shared. - -const fs = require("fs-extra"); -const glob = require("glob"); -const zip = require('bestzip'); -const path = require("path"); -const minimatch = require('minimatch'); - -// Load the package.json file to get some information about the package so we can name things appropriately. This is -// atypical, and you would never do this in a production environment, but this script is only used for development so -// it's fine in this case. Some of these values are stored in environment variables, but those differ between node -// versions; the 'author' value is not available after node v14. -const { author, name:packageName, version } = require("./package.json"); - -// Generate the name of the package, stripping out all non-alphanumeric characters in the 'author' and 'name'. -const modName = `${author.replace(/[^a-z0-9]/gi, "")}-${packageName.replace(/[^a-z0-9]/gi, "")}-${version}`; -console.log(`Generated package name: ${modName}`); - -// Delete the old build directory and compressed package file. -fs.rmSync(`${__dirname}/dist`, { force: true, recursive: true }); -console.log("Previous build files deleted."); - -// Generate a list of files that should not be copied over into the distribution directory. This is a blacklist to ensure -// we always copy over additional files and directories that authors may have added to their project. This may need to be -// expanded upon by the mod author to allow for node modules that are used within the mod; example commented out below. -const ignoreList = [ - "node_modules/", - // "node_modules/!(weighted|glob)", // Instead of excluding the entire node_modules directory, allow two node modules. - "src/**/*.js", - "types/", - ".git/", - ".gitea/", - ".eslintignore", - ".eslintrc.json", - ".gitignore", - ".DS_Store", - "packageBuild.ts", - "mod.code-workspace", - "package-lock.json", - "tsconfig.json" -]; -const exclude = glob.sync(`{${ignoreList.join(",")}}`, { realpath: true, dot: true }); - -fs.copySync(__dirname, path.normalize(`${__dirname}/../~${modName}`), {filter: (src) => -{ - const relativePath = path.relative(__dirname, src); - const shouldExclude = exclude.some((pattern) => minimatch(relativePath, pattern)); - console.log(`${relativePath} - Excluded: ${shouldExclude}`); - return !shouldExclude; -},}); - -fs.moveSync(path.normalize(`${__dirname}/../~${modName}`), path.normalize(`${__dirname}/${modName}`), { overwrite: true }); -fs.copySync(path.normalize(`${__dirname}/${modName}`), path.normalize(`${__dirname}/dist`)); -console.log("Build files copied."); - -// Compress the files for easy distribution. The compressed file is saved into the dist directory. When uncompressed we -// need to be sure that it includes a directory that the user can easily copy into their game mods directory. -zip({ - source: modName, - destination: `dist/${modName}.zip`, - cwd: __dirname -}).catch(function(err) -{ - console.error("A bestzip error has occurred: ", err.stack); -}).then(function() -{ - console.log(`Compressed mod package to: /dist/${modName}.zip`); - - // Now that we're done with the compression we can delete the temporary build directory. - fs.rmSync(`${__dirname}/${modName}`, { force: true, recursive: true }); - console.log("Build successful! your zip file has been created and is ready to be uploaded to hub.sp-tarkov.com/files/"); -}); \ No newline at end of file diff --git a/TypeScript/13AddTrader/tsconfig.json b/TypeScript/13AddTrader/tsconfig.json index 057f145..ed64bc7 100644 --- a/TypeScript/13AddTrader/tsconfig.json +++ b/TypeScript/13AddTrader/tsconfig.json @@ -8,7 +8,7 @@ "downlevelIteration": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, - "resolveJsonModule": true, + "resolveJsonModule": true, "outDir": "tmp", "baseUrl": ".", "paths": { diff --git a/TypeScript/14AfterDBLoadHook/.buildignore b/TypeScript/14AfterDBLoadHook/.buildignore new file mode 100644 index 0000000..2cbde65 --- /dev/null +++ b/TypeScript/14AfterDBLoadHook/.buildignore @@ -0,0 +1,20 @@ +/.buildignore +/.DS_Store +/.editorconfig +/.eslintignore +/.eslintrc.json +/.git +/.github +/.gitignore +/.gitlab +/.nvmrc +/.prettierrc +/.vscode +/build.mjs +/dist +/images +/mod.code-workspace +/node_modules +/package-lock.json +/tsconfig.json +/types diff --git a/TypeScript/14AfterDBLoadHook/.eslintrc.json b/TypeScript/14AfterDBLoadHook/.eslintrc.json index c505160..071a313 100644 --- a/TypeScript/14AfterDBLoadHook/.eslintrc.json +++ b/TypeScript/14AfterDBLoadHook/.eslintrc.json @@ -1,75 +1,98 @@ { - "root": true, - "parser": "@typescript-eslint/parser", - "plugins": [ - "@typescript-eslint" - ], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended" - ], - "rules": { - "@typescript-eslint/no-explicit-any": 0, - "@typescript-eslint/no-unused-vars": 1, - "@typescript-eslint/no-empty-interface": 0, - "@typescript-eslint/no-namespace": 0, - "@typescript-eslint/comma-dangle": 1, - "@typescript-eslint/func-call-spacing": 2, - "@typescript-eslint/quotes": 1, - "@typescript-eslint/brace-style": [ - "warn", - "allman" + "root": true, + "parser": "@typescript-eslint/parser", + "plugins": [ + "@typescript-eslint" ], - "@typescript-eslint/naming-convention": [ - "warn", - { - "selector": "default", - "format": ["camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "typeLike", - "format": ["PascalCase"] - }, - { - "selector": "objectLiteralProperty", - "format": ["PascalCase", "camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "typeProperty", - "format": ["PascalCase", "camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "enumMember", - "format": ["UPPER_CASE"] - } + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" ], - "@typescript-eslint/indent": [ - "warn", - 4 - ], - "@typescript-eslint/no-unused-expressions": [ - "warn", - { - "allowShortCircuit": false, - "allowTernary": false - } - ], - "@typescript-eslint/keyword-spacing": [ - "warn", - { - "before": true, - "after": true - } - ], - "@typescript-eslint/explicit-module-boundary-types": [ - "warn", - { - "allowArgumentsExplicitlyTypedAsAny": true - } + "rules": { + "@typescript-eslint/no-explicit-any": 0, + "@typescript-eslint/no-unused-vars": 1, + "@typescript-eslint/no-empty-interface": 0, + "@typescript-eslint/no-namespace": 0, + "@typescript-eslint/comma-dangle": 1, + "@typescript-eslint/func-call-spacing": 2, + "@typescript-eslint/quotes": 1, + "@typescript-eslint/brace-style": [ + "warn", + "allman" + ], + "@typescript-eslint/naming-convention": [ + "warn", + { + "selector": "default", + "format": [ + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "typeLike", + "format": [ + "PascalCase" + ] + }, + { + "selector": "objectLiteralProperty", + "format": [ + "PascalCase", + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "typeProperty", + "format": [ + "PascalCase", + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "enumMember", + "format": [ + "UPPER_CASE" + ] + } + ], + "@typescript-eslint/indent": [ + "warn", + 4 + ], + "@typescript-eslint/no-unused-expressions": [ + "warn", + { + "allowShortCircuit": false, + "allowTernary": false + } + ], + "@typescript-eslint/keyword-spacing": [ + "warn", + { + "before": true, + "after": true + } + ], + "@typescript-eslint/explicit-module-boundary-types": [ + "warn", + { + "allowArgumentsExplicitlyTypedAsAny": true + } + ] + }, + "overrides": [ + { + "files": [ + "*.mjs", + "*.ts" + ], + "env": { + "node": true + } + } ] - } } \ No newline at end of file diff --git a/TypeScript/14AfterDBLoadHook/README.md b/TypeScript/14AfterDBLoadHook/README.md index 434d063..8cba28e 100644 --- a/TypeScript/14AfterDBLoadHook/README.md +++ b/TypeScript/14AfterDBLoadHook/README.md @@ -1,64 +1,66 @@ +# Welcome to the SPT-AKI Modding Project -This project was created to automate most parts of building and setting up an environment. +This project is designed to streamline the initial setup process for building and creating mods in the SPT-AKI environment. Follow this guide to set up your environment efficiently. -## **NodeJS:** +## **Table of Contents** +- [NodeJS Setup](#nodejs-setup) +- [IDE Setup](#ide-setup) +- [Workspace Configuration](#workspace-configuration) +- [Environment Setup](#environment-setup) +- [Essential Concepts](#essential-concepts) +- [Coding Guidelines](#coding-guidelines) +- [Distribution Guidelines](#distribution-guidelines) -The first step would be to install nodejs on your pc, the version you NEED is **16.17.1** +## **NodeJS Setup** -That version is the one that has been used to test the mod templates and build scripts. +Before you begin, ensure to install NodeJS version `v16.17.1`, which has been tested thoroughly with our mod templates and build scripts. Download it from the [official NodeJS website](https://nodejs.org/). -It can be downloaded from here: https://nodejs.org/dist/v16.17.1/node-v16.17.1-x64.msi +After installation, it's advised to reboot your system. -A system reboot may be needed after install. +## **IDE Setup** -## **IDE:** +For this project, you can work with either [VSCodium](https://vscodium.com/) or [VSCode](https://code.visualstudio.com/). However, we strongly recommend using VSCode, as all development and testing have been carried out using this IDE, ensuring a smoother experience and compatibility with the project setups. Either way, we have a prepared a workspace file to assist you in setting up your environment. -The second step is having an IDE ready. We've setup a VSCodium workspace file to help with this. +## **Workspace Configuration** -You CAN use Visual Studio Code if you so desire, just keep in mind that our devs tests on the mod files was done using VSCode. +With NodeJS and your chosen IDE ready, initiate the `mod.code-workspace` file using your IDE: -You can get VSCode here: https://code.visualstudio.com/ +> File -> Open Workspace from File... -## **Workspace:** +Upon project loading, consider installing recommended plugins like the ESLint plugin. -Once you have NodeJS and VSCode/VSCodium ready, open the mod.code-workspace file with VSCode (File->Open Workspace from File...). +## **Environment Setup** -Once the project loads you may be recommended to install the ESLint plugin. This is HIGHLY recommended. +An automated task is available to configure your environment for Typescript utilization: -## **Environment Setup:** +> Terminal -> Run Task... -> Show All Tasks... -> npm: install -There is a task that will automatically setup your environment to use typescript. +Note: Preserve the `node_modules` folder as it contains necessary dependencies for Typescript and other functionalities. -To run it, you just need to go to: +## **Essential Concepts** -> Terminal->Run Task...->Show All Tasks...->npm: install +Prioritize understanding Dependency Injection and Inversion of Control, the architectural principles SPT-AKI adopts. Comprehensive guidelines will be available on the hub upon release. -After running this task, your environment will be ready to start coding. +Some resources to get you started: + - [A quick intro to Dependency Injection](https://www.freecodecamp.org/news/a-quick-intro-to-dependency-injection-what-it-is-and-when-to-use-it-7578c84fa88f/) + - [Understanding Inversion of Control (IoC) Principle](https://medium.com/@amitkma/understanding-inversion-of-control-ioc-principle-163b1dc97454) -DO NOT remove the node_modules folder, this is an auto generated directory that has the required dependencies to be able to use typescript and more. +## **Coding Guidelines** -## **IMPORTANT:** +Focus your mod development around the `mod.ts` file. In the `package.json` file, only alter these properties: `"name"`, `"version"`, `"license"`, `"author"`, and `"akiVersion"`. -Before starting to work on your mod, we suggest you read about Dependency Injection and Inversion of Control as this is the adopted architecture SPT-AKI has adopted. +New to Typescript? Find comprehensive documentation on the [official website](https://www.typescriptlang.org/docs/). -It will be difficult to understand some of the problems you may be having if you dont understand the basics of it. +## **Distribution Guidelines** -A guide explaining all the essentials will be available on the hub on release for you to read about. +Automated tasks are set up to bundle all necessary files for your mod to function in SPT-AKI: -## **Coding:** +> Terminal -> Run Task... -> Show All Tasks... -> npm: build -All your work should be centered around the mod.ts file as an entry point. -You can ONLY change the following properties from the package.json file: `"name"`, `"version"`, `"license"`: `"MIT"`, `"author"`, `"akiVersion"`. +The ZIP output, located in the `dist` directory, contains all required files. Ensure all files are included and modify the `.buildignore` file as needed. This ZIP file is your uploadable asset for the hub. -If you have never used typescript before, you can read about it here: https://www.typescriptlang.org/docs/ +## **Conclusion** -## **Distributing your mod:** +With this setup, you're ready to begin modding with SPT-AKI. If you run into any trouble be sure to check out the [modding documentation on the hub](https://hub.sp-tarkov.com/doc/lexicon/66-modding/). If you really get stuck feel free to join us in the [#mods-development](https://discord.com/channels/875684761291599922/875803116409323562) official Discord channel. -The project has been set up with an automatic task that will copy and zip ALL required files for your mod to work on SPT-AKI. -To run this task you just need to go to: - -> Terminal->Run Task...->Show All Tasks...->npm: build:zip - -The output will be a mod.zip file that will appear on the root of the project. - -Always verify that all files were included into the zip file. \ No newline at end of file +Build something awesome! diff --git a/TypeScript/14AfterDBLoadHook/build.mjs b/TypeScript/14AfterDBLoadHook/build.mjs new file mode 100644 index 0000000..d00117c --- /dev/null +++ b/TypeScript/14AfterDBLoadHook/build.mjs @@ -0,0 +1,383 @@ +#!/usr/bin/env node + +/** + * Build Script + * + * This script automates the build process for server-side SPT mod projects, facilitating the creation of distributable + * mod packages. It performs a series of operations as outlined below: + * - Loads the .buildignore file, which is used to list files that should be ignored during the build process. + * - Loads the package.json to get project details so a descriptive name can be created for the mod package. + * - Creates a distribution directory and a temporary working directory. + * - Copies files to the temporary directory while respecting the .buildignore rules. + * - Creates a zip archive of the project files. + * - Moves the zip file to the root of the distribution directory. + * - Cleans up the temporary directory. + * + * It's typical that this script be customized to suit the needs of each project. For example, the script can be updated + * to perform additional operations, such as moving the mod package to a specific location or uploading it to a server. + * This script is intended to be a starting point for developers to build upon. + * + * Usage: + * - Run this script using npm: `npm run build` + * - Use `npm run buildinfo` for detailed logging. + * + * Note: + * - Ensure that all necessary Node.js modules are installed before running the script: `npm install` + * - The script reads configurations from the `package.json` and `.buildignore` files; ensure they are correctly set up. + * + * @author Refringe + * @version v1.0.0 + */ + +import fs from "fs-extra"; +import os from "os"; +import path from "path"; +import { fileURLToPath } from "url"; +import { dirname } from "path"; +import ignore from "ignore"; +import archiver from "archiver"; +import winston from "winston"; + +// Get the command line arguments to determine whether to use verbose logging. +const args = process.argv.slice(2); +const verbose = args.includes("--verbose") || args.includes("-v"); + +// Configure the Winston logger to use colours. +const logColors = { + error: "red", + warn: "yellow", + info: "grey", + success: "green", +}; +winston.addColors(logColors); + +// Create a logger instance to log build progress. Configure the logger levels to allow for different levels of logging +// based on the verbosity flag, and set the console transport to log messages of the appropriate level. +const logger = winston.createLogger({ + levels: { + error: 0, + warn: 1, + success: 2, + info: 3, + }, + format: winston.format.combine( + winston.format.colorize(), + winston.format.printf(info => { + return `${info.level}: ${info.message}`; + }) + ), + transports: [ + new winston.transports.Console({ + level: verbose ? "info" : "success", + }), + ], +}); + +/** + * The main function orchestrates the build process for creating a distributable mod package. It leverages a series of + * helper functions to perform various tasks such as loading configuration files, setting up directories, copying files + * according to `.buildignore` rules, and creating a ZIP archive of the project files. + * + * Utilizes the Winston logger to provide information on the build status at different stages of the process. + * + * @returns {void} + */ +async function main() { + // Get the current directory where the script is being executed + const currentDir = getCurrentDirectory(); + + // Defining at this scope because we need to use it in the finally block. + let projectDir; + + try { + // Load the .buildignore file to set up an ignore handler for the build process. + const buildIgnorePatterns = await loadBuildIgnoreFile(currentDir); + + // Load the package.json file to get project details. + const packageJson = await loadPackageJson(currentDir); + + // Create a descriptive name for the mod package. + const projectName = createProjectName(packageJson); + logger.log("success", `Project name created: ${projectName}`); + + // Remove the old distribution directory and create a fresh one. + const distDir = await removeOldDistDirectory(currentDir); + logger.log("info", "Distribution directory successfully cleaned."); + + // Create a temporary working directory to perform the build operations. + projectDir = await createTemporaryDirectoryWithProjectName(projectName); + logger.log("success", "Temporary working directory successfully created."); + logger.log("info", projectDir); + + // Copy files to the temporary directory while respecting the .buildignore rules. + logger.log("info", "Beginning copy operation using .buildignore file..."); + await copyFiles(currentDir, projectDir, buildIgnorePatterns); + logger.log("success", "Files successfully copied to temporary directory."); + + // Create a zip archive of the project files. + logger.log("info", "Beginning folder compression..."); + const zipFilePath = path.join(path.dirname(projectDir), `${projectName}.zip`); + await createZipFile(projectDir, zipFilePath, "user/mods/" + projectName); + logger.log("success", "Archive successfully created."); + logger.log("info", zipFilePath); + + // Move the zip file inside of the project directory, within the temporary working directory. + const zipFileInProjectDir = path.join(projectDir, `${projectName}.zip`); + await fs.move(zipFilePath, zipFileInProjectDir); + logger.log("success", "Archive successfully moved."); + logger.log("info", zipFileInProjectDir); + + // Move the temporary directory into the distribution directory. + await fs.move(projectDir, distDir); + logger.log("success", "Temporary directory successfully moved into project distribution directory."); + + // Log the success message. Write out the path to the mod package. + logger.log("success", "------------------------------------"); + logger.log("success", "Build script completed successfully!"); + logger.log("success", "Your mod package has been created in the 'dist' directory:"); + logger.log("success", `/${path.relative(process.cwd(), path.join(distDir, `${projectName}.zip`))}`); + logger.log("success", "------------------------------------"); + if (!verbose) { + logger.log("success", "To see a detailed build log, use `npm run buildinfo`."); + logger.log("success", "------------------------------------"); + } + } catch (err) { + // If any of the file operations fail, log the error. + logger.log("error", "An error occurred: " + err); + } finally { + // Clean up the temporary directory, even if the build fails. + if (projectDir) { + try { + await fs.promises.rm(projectDir, { force: true, recursive: true }); + logger.log("info", "Cleaned temporary directory."); + } catch (err) { + logger.log("error", "Failed to clean temporary directory: " + err); + } + } + } +} + +/** + * Retrieves the current working directory where the script is being executed. This directory is used as a reference + * point for various file operations throughout the build process, ensuring that paths are resolved correctly regardless + * of the location from which the script is invoked. + * + * @returns {string} The absolute path of the current working directory. + */ +function getCurrentDirectory() { + return dirname(fileURLToPath(import.meta.url)); +} + +/** + * Loads the `.buildignore` file and sets up an ignore handler using the `ignore` module. The `.buildignore` file + * contains a list of patterns describing files and directories that should be ignored during the build process. The + * ignore handler created by this method is used to filter files and directories when copying them to the temporary + * directory, ensuring that only necessary files are included in the final mod package. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to an ignore handler. + */ +async function loadBuildIgnoreFile(currentDir) { + const buildIgnorePath = path.join(currentDir, ".buildignore"); + + try { + // Attempt to read the contents of the .buildignore file asynchronously. + const fileContent = await fs.promises.readFile(buildIgnorePath, "utf-8"); + + // Return a new ignore instance and add the rules from the .buildignore file (split by newlines). + return ignore().add(fileContent.split("\n")); + } catch (err) { + logger.log("warn", "Failed to read .buildignore file. No files or directories will be ignored."); + + // Return an empty ignore instance, ensuring the build process can continue. + return ignore(); + } +} + +/** + * Loads the `package.json` file and returns its content as a JSON object. The `package.json` file contains important + * project details such as the name and version, which are used in later stages of the build process to create a + * descriptive name for the mod package. The method reads the file from the current working directory, ensuring that it + * accurately reflects the current state of the project. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to a JSON object containing the contents of the `package.json`. + */ +async function loadPackageJson(currentDir) { + const packageJsonPath = path.join(currentDir, "package.json"); + + // Read the contents of the package.json file asynchronously as a UTF-8 string. + const packageJsonContent = await fs.promises.readFile(packageJsonPath, "utf-8"); + + return JSON.parse(packageJsonContent); +} + +/** + * Constructs a descriptive name for the mod package using details from the `package.json` file. The name is created by + * concatenating the project name, version, and a timestamp, resulting in a unique and descriptive file name for each + * build. This name is used as the base name for the temporary working directory and the final ZIP archive, helping to + * identify different versions of the mod package easily. + * + * @param {Object} packageJson - A JSON object containing the contents of the `package.json` file. + * @returns {string} A string representing the constructed project name. + */ +function createProjectName(packageJson) { + // Remove any non-alphanumeric characters from the author and name. + const author = packageJson.author.replace(/\W/g, ""); + const name = packageJson.name.replace(/\W/g, ""); + const version = packageJson.version; + + // Ensure the name is lowercase, as per the package.json specification. + return `${author}-${name}-${version}`.toLowerCase(); +} + +/** + * Defines the location of the distribution directory where the final mod package will be stored and deletes any + * existing distribution directory to ensure a clean slate for the build process. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to the absolute path to the distribution directory. + */ +async function removeOldDistDirectory(projectDir) { + const distPath = path.join(projectDir, "dist"); + await fs.remove(distPath); + return distPath; +} + +/** + * Creates a temporary working directory using the project name. This directory serves as a staging area where project + * files are gathered before being archived into the final mod package. The method constructs a unique directory path + * by appending the project name to a base temporary directory path, ensuring that each build has its own isolated + * working space. This approach facilitates clean and organized build processes, avoiding potential conflicts with other + * builds. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @param {string} projectName - The constructed project name, used to create a unique path for the temporary directory. + * @returns {Promise} A promise that resolves to the absolute path of the newly created temporary directory. + */ +async function createTemporaryDirectoryWithProjectName(projectName) { + // Create a new directory in the system's temporary folder to hold the project files. + const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "spt-mod-build-")); + + // Create a subdirectory within the temporary directory using the project name for this specific build. + const projectDir = path.join(tempDir, projectName); + await fs.ensureDir(projectDir); + + return projectDir; +} + +/** + * Copies the project files to the temporary directory while respecting the rules defined in the `.buildignore` file. + * The method is recursive, iterating over all files and directories in the source directory and using the ignore + * handler to filter out files and directories that match the patterns defined in the `.buildignore` file. This ensures + * that only the necessary files are included in the final mod package, adhering to the specifications defined by the + * developer in the `.buildignore` file. + * + * The copy operations are delayed and executed in parallel to improve efficiency and reduce the build time. This is + * achieved by creating an array of copy promises and awaiting them all at the end of the function. + * + * @param {string} sourceDirectory - The absolute path of the current working directory. + * @param {string} destinationDirectory - The absolute path of the temporary directory where the files will be copied. + * @param {Ignore} ignoreHandler - The ignore handler created from the `.buildignore` file. + * @returns {Promise} A promise that resolves when all copy operations are completed successfully. + */ +async function copyFiles(srcDir, destDir, ignoreHandler) { + try { + // Read the contents of the source directory to get a list of entries (files and directories). + const entries = await fs.promises.readdir(srcDir, { withFileTypes: true }); + + // Initialize an array to hold the promises returned by recursive calls to copyFiles and copyFile operations. + const copyOperations = []; + + for (const entry of entries) { + // Define the source and destination paths for each entry. + const srcPath = path.join(srcDir, entry.name); + const destPath = path.join(destDir, entry.name); + + // Get the relative path of the source file to check against the ignore handler. + const relativePath = path.relative(process.cwd(), srcPath); + + // If the ignore handler dictates that this file should be ignored, skip to the next iteration. + if (ignoreHandler.ignores(relativePath)) { + logger.log("info", `Ignored: /${path.relative(process.cwd(), srcPath)}`); + continue; + } + + if (entry.isDirectory()) { + // If the entry is a directory, create the corresponding temporary directory and make a recursive call + // to copyFiles to handle copying the contents of the directory. + await fs.ensureDir(destPath); + copyOperations.push(copyFiles(srcPath, destPath, ignoreHandler)); + } else { + // If the entry is a file, add a copyFile operation to the copyOperations array and log the event when + // the operation is successful. + copyOperations.push( + fs.copy(srcPath, destPath).then(() => { + logger.log("info", `Copied: /${path.relative(process.cwd(), srcPath)}`); + }) + ); + } + } + + // Await all copy operations to ensure all files and directories are copied before exiting the function. + await Promise.all(copyOperations); + } catch (err) { + // Log an error message if any error occurs during the copy process. + logger.log("error", "Error copying files: " + err); + } +} + +/** + * Creates a ZIP archive of the project files located in the temporary directory. The method uses the `archiver` module + * to create a ZIP file, which includes all the files that have been copied to the temporary directory during the build + * process. The ZIP file is named using the project name, helping to identify the contents of the archive easily. + * + * @param {string} directoryPath - The absolute path of the temporary directory containing the project files. + * @param {string} projectName - The constructed project name, used to name the ZIP file. + * @returns {Promise} A promise that resolves to the absolute path of the created ZIP file. + */ +async function createZipFile(directoryToZip, zipFilePath, containerDirName) { + return new Promise((resolve, reject) => { + // Create a write stream to the specified ZIP file path. + const output = fs.createWriteStream(zipFilePath); + + // Create a new archiver instance with ZIP format and maximum compression level. + const archive = archiver("zip", { + zlib: { level: 9 }, + }); + + // Set up an event listener for the 'close' event to resolve the promise when the archiver has finalized. + output.on("close", function () { + logger.log("info", "Archiver has finalized. The output and the file descriptor have closed."); + resolve(); + }); + + // Set up an event listener for the 'warning' event to handle warnings appropriately, logging them or rejecting + // the promise based on the error code. + archive.on("warning", function (err) { + if (err.code === "ENOENT") { + logger.log("warn", `Archiver issued a warning: ${err.code} - ${err.message}`); + } else { + reject(err); + } + }); + + // Set up an event listener for the 'error' event to reject the promise if any error occurs during archiving. + archive.on("error", function (err) { + reject(err); + }); + + // Pipe archive data to the file. + archive.pipe(output); + + // Add the directory to the archive, under the provided directory name. + archive.directory(directoryToZip, containerDirName); + + // Finalize the archive, indicating that no more files will be added and triggering the 'close' event once all + // data has been written. + archive.finalize(); + }); +} + +// Engage! +main(); diff --git a/TypeScript/14AfterDBLoadHook/package.json b/TypeScript/14AfterDBLoadHook/package.json index ad486dd..31d33d4 100644 --- a/TypeScript/14AfterDBLoadHook/package.json +++ b/TypeScript/14AfterDBLoadHook/package.json @@ -7,17 +7,20 @@ "akiVersion": "~3.7", "scripts": { "setup": "npm i", - "build": "node ./packageBuild.ts" + "build": "node ./build.mjs", + "buildinfo": "node ./build.mjs --verbose" }, "devDependencies": { "@types/node": "20.4.5", "@typescript-eslint/eslint-plugin": "6.2.0", "@typescript-eslint/parser": "6.2.0", - "bestzip": "2.2.1", + "archiver": "^6.0", "eslint": "8.46.0", - "fs-extra": "11.1.1", - "glob": "10.3.3", + "fs-extra": "^11.1", + "ignore": "^5.2", + "os": "^0.1", "tsyringe": "4.8.0", - "typescript": "5.1.6" + "typescript": "5.1.6", + "winston": "^3.9" } -} \ No newline at end of file +} diff --git a/TypeScript/14AfterDBLoadHook/packageBuild.ts b/TypeScript/14AfterDBLoadHook/packageBuild.ts deleted file mode 100644 index d76ddd8..0000000 --- a/TypeScript/14AfterDBLoadHook/packageBuild.ts +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env node - -// This is a simple script used to build a mod package. The script will copy necessary files to the build directory -// and compress the build directory into a zip file that can be easily shared. - -const fs = require("fs-extra"); -const glob = require("glob"); -const zip = require('bestzip'); -const path = require("path"); -const minimatch = require('minimatch'); - -// Load the package.json file to get some information about the package so we can name things appropriately. This is -// atypical, and you would never do this in a production environment, but this script is only used for development so -// it's fine in this case. Some of these values are stored in environment variables, but those differ between node -// versions; the 'author' value is not available after node v14. -const { author, name:packageName, version } = require("./package.json"); - -// Generate the name of the package, stripping out all non-alphanumeric characters in the 'author' and 'name'. -const modName = `${author.replace(/[^a-z0-9]/gi, "")}-${packageName.replace(/[^a-z0-9]/gi, "")}-${version}`; -console.log(`Generated package name: ${modName}`); - -// Delete the old build directory and compressed package file. -fs.rmSync(`${__dirname}/dist`, { force: true, recursive: true }); -console.log("Previous build files deleted."); - -// Generate a list of files that should not be copied over into the distribution directory. This is a blacklist to ensure -// we always copy over additional files and directories that authors may have added to their project. This may need to be -// expanded upon by the mod author to allow for node modules that are used within the mod; example commented out below. -const ignoreList = [ - "node_modules/", - // "node_modules/!(weighted|glob)", // Instead of excluding the entire node_modules directory, allow two node modules. - "src/**/*.js", - "types/", - ".git/", - ".gitea/", - ".eslintignore", - ".eslintrc.json", - ".gitignore", - ".DS_Store", - "packageBuild.ts", - "mod.code-workspace", - "package-lock.json", - "tsconfig.json" -]; -const exclude = glob.sync(`{${ignoreList.join(",")}}`, { realpath: true, dot: true }); - -fs.copySync(__dirname, path.normalize(`${__dirname}/../~${modName}`), {filter: (src) => -{ - const relativePath = path.relative(__dirname, src); - const shouldExclude = exclude.some((pattern) => minimatch(relativePath, pattern)); - console.log(`${relativePath} - Excluded: ${shouldExclude}`); - return !shouldExclude; -},}); - -fs.moveSync(path.normalize(`${__dirname}/../~${modName}`), path.normalize(`${__dirname}/${modName}`), { overwrite: true }); -fs.copySync(path.normalize(`${__dirname}/${modName}`), path.normalize(`${__dirname}/dist`)); -console.log("Build files copied."); - -// Compress the files for easy distribution. The compressed file is saved into the dist directory. When uncompressed we -// need to be sure that it includes a directory that the user can easily copy into their game mods directory. -zip({ - source: modName, - destination: `dist/${modName}.zip`, - cwd: __dirname -}).catch(function(err) -{ - console.error("A bestzip error has occurred: ", err.stack); -}).then(function() -{ - console.log(`Compressed mod package to: /dist/${modName}.zip`); - - // Now that we're done with the compression we can delete the temporary build directory. - fs.rmSync(`${__dirname}/${modName}`, { force: true, recursive: true }); - console.log("Build successful! your zip file has been created and is ready to be uploaded to hub.sp-tarkov.com/files/"); -}); \ No newline at end of file diff --git a/TypeScript/15HttpListenerExample/.buildignore b/TypeScript/15HttpListenerExample/.buildignore new file mode 100644 index 0000000..2cbde65 --- /dev/null +++ b/TypeScript/15HttpListenerExample/.buildignore @@ -0,0 +1,20 @@ +/.buildignore +/.DS_Store +/.editorconfig +/.eslintignore +/.eslintrc.json +/.git +/.github +/.gitignore +/.gitlab +/.nvmrc +/.prettierrc +/.vscode +/build.mjs +/dist +/images +/mod.code-workspace +/node_modules +/package-lock.json +/tsconfig.json +/types diff --git a/TypeScript/15HttpListenerExample/.eslintrc.json b/TypeScript/15HttpListenerExample/.eslintrc.json index c505160..071a313 100644 --- a/TypeScript/15HttpListenerExample/.eslintrc.json +++ b/TypeScript/15HttpListenerExample/.eslintrc.json @@ -1,75 +1,98 @@ { - "root": true, - "parser": "@typescript-eslint/parser", - "plugins": [ - "@typescript-eslint" - ], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended" - ], - "rules": { - "@typescript-eslint/no-explicit-any": 0, - "@typescript-eslint/no-unused-vars": 1, - "@typescript-eslint/no-empty-interface": 0, - "@typescript-eslint/no-namespace": 0, - "@typescript-eslint/comma-dangle": 1, - "@typescript-eslint/func-call-spacing": 2, - "@typescript-eslint/quotes": 1, - "@typescript-eslint/brace-style": [ - "warn", - "allman" + "root": true, + "parser": "@typescript-eslint/parser", + "plugins": [ + "@typescript-eslint" ], - "@typescript-eslint/naming-convention": [ - "warn", - { - "selector": "default", - "format": ["camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "typeLike", - "format": ["PascalCase"] - }, - { - "selector": "objectLiteralProperty", - "format": ["PascalCase", "camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "typeProperty", - "format": ["PascalCase", "camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "enumMember", - "format": ["UPPER_CASE"] - } + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" ], - "@typescript-eslint/indent": [ - "warn", - 4 - ], - "@typescript-eslint/no-unused-expressions": [ - "warn", - { - "allowShortCircuit": false, - "allowTernary": false - } - ], - "@typescript-eslint/keyword-spacing": [ - "warn", - { - "before": true, - "after": true - } - ], - "@typescript-eslint/explicit-module-boundary-types": [ - "warn", - { - "allowArgumentsExplicitlyTypedAsAny": true - } + "rules": { + "@typescript-eslint/no-explicit-any": 0, + "@typescript-eslint/no-unused-vars": 1, + "@typescript-eslint/no-empty-interface": 0, + "@typescript-eslint/no-namespace": 0, + "@typescript-eslint/comma-dangle": 1, + "@typescript-eslint/func-call-spacing": 2, + "@typescript-eslint/quotes": 1, + "@typescript-eslint/brace-style": [ + "warn", + "allman" + ], + "@typescript-eslint/naming-convention": [ + "warn", + { + "selector": "default", + "format": [ + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "typeLike", + "format": [ + "PascalCase" + ] + }, + { + "selector": "objectLiteralProperty", + "format": [ + "PascalCase", + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "typeProperty", + "format": [ + "PascalCase", + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "enumMember", + "format": [ + "UPPER_CASE" + ] + } + ], + "@typescript-eslint/indent": [ + "warn", + 4 + ], + "@typescript-eslint/no-unused-expressions": [ + "warn", + { + "allowShortCircuit": false, + "allowTernary": false + } + ], + "@typescript-eslint/keyword-spacing": [ + "warn", + { + "before": true, + "after": true + } + ], + "@typescript-eslint/explicit-module-boundary-types": [ + "warn", + { + "allowArgumentsExplicitlyTypedAsAny": true + } + ] + }, + "overrides": [ + { + "files": [ + "*.mjs", + "*.ts" + ], + "env": { + "node": true + } + } ] - } } \ No newline at end of file diff --git a/TypeScript/15HttpListenerExample/README.md b/TypeScript/15HttpListenerExample/README.md index 434d063..8cba28e 100644 --- a/TypeScript/15HttpListenerExample/README.md +++ b/TypeScript/15HttpListenerExample/README.md @@ -1,64 +1,66 @@ +# Welcome to the SPT-AKI Modding Project -This project was created to automate most parts of building and setting up an environment. +This project is designed to streamline the initial setup process for building and creating mods in the SPT-AKI environment. Follow this guide to set up your environment efficiently. -## **NodeJS:** +## **Table of Contents** +- [NodeJS Setup](#nodejs-setup) +- [IDE Setup](#ide-setup) +- [Workspace Configuration](#workspace-configuration) +- [Environment Setup](#environment-setup) +- [Essential Concepts](#essential-concepts) +- [Coding Guidelines](#coding-guidelines) +- [Distribution Guidelines](#distribution-guidelines) -The first step would be to install nodejs on your pc, the version you NEED is **16.17.1** +## **NodeJS Setup** -That version is the one that has been used to test the mod templates and build scripts. +Before you begin, ensure to install NodeJS version `v16.17.1`, which has been tested thoroughly with our mod templates and build scripts. Download it from the [official NodeJS website](https://nodejs.org/). -It can be downloaded from here: https://nodejs.org/dist/v16.17.1/node-v16.17.1-x64.msi +After installation, it's advised to reboot your system. -A system reboot may be needed after install. +## **IDE Setup** -## **IDE:** +For this project, you can work with either [VSCodium](https://vscodium.com/) or [VSCode](https://code.visualstudio.com/). However, we strongly recommend using VSCode, as all development and testing have been carried out using this IDE, ensuring a smoother experience and compatibility with the project setups. Either way, we have a prepared a workspace file to assist you in setting up your environment. -The second step is having an IDE ready. We've setup a VSCodium workspace file to help with this. +## **Workspace Configuration** -You CAN use Visual Studio Code if you so desire, just keep in mind that our devs tests on the mod files was done using VSCode. +With NodeJS and your chosen IDE ready, initiate the `mod.code-workspace` file using your IDE: -You can get VSCode here: https://code.visualstudio.com/ +> File -> Open Workspace from File... -## **Workspace:** +Upon project loading, consider installing recommended plugins like the ESLint plugin. -Once you have NodeJS and VSCode/VSCodium ready, open the mod.code-workspace file with VSCode (File->Open Workspace from File...). +## **Environment Setup** -Once the project loads you may be recommended to install the ESLint plugin. This is HIGHLY recommended. +An automated task is available to configure your environment for Typescript utilization: -## **Environment Setup:** +> Terminal -> Run Task... -> Show All Tasks... -> npm: install -There is a task that will automatically setup your environment to use typescript. +Note: Preserve the `node_modules` folder as it contains necessary dependencies for Typescript and other functionalities. -To run it, you just need to go to: +## **Essential Concepts** -> Terminal->Run Task...->Show All Tasks...->npm: install +Prioritize understanding Dependency Injection and Inversion of Control, the architectural principles SPT-AKI adopts. Comprehensive guidelines will be available on the hub upon release. -After running this task, your environment will be ready to start coding. +Some resources to get you started: + - [A quick intro to Dependency Injection](https://www.freecodecamp.org/news/a-quick-intro-to-dependency-injection-what-it-is-and-when-to-use-it-7578c84fa88f/) + - [Understanding Inversion of Control (IoC) Principle](https://medium.com/@amitkma/understanding-inversion-of-control-ioc-principle-163b1dc97454) -DO NOT remove the node_modules folder, this is an auto generated directory that has the required dependencies to be able to use typescript and more. +## **Coding Guidelines** -## **IMPORTANT:** +Focus your mod development around the `mod.ts` file. In the `package.json` file, only alter these properties: `"name"`, `"version"`, `"license"`, `"author"`, and `"akiVersion"`. -Before starting to work on your mod, we suggest you read about Dependency Injection and Inversion of Control as this is the adopted architecture SPT-AKI has adopted. +New to Typescript? Find comprehensive documentation on the [official website](https://www.typescriptlang.org/docs/). -It will be difficult to understand some of the problems you may be having if you dont understand the basics of it. +## **Distribution Guidelines** -A guide explaining all the essentials will be available on the hub on release for you to read about. +Automated tasks are set up to bundle all necessary files for your mod to function in SPT-AKI: -## **Coding:** +> Terminal -> Run Task... -> Show All Tasks... -> npm: build -All your work should be centered around the mod.ts file as an entry point. -You can ONLY change the following properties from the package.json file: `"name"`, `"version"`, `"license"`: `"MIT"`, `"author"`, `"akiVersion"`. +The ZIP output, located in the `dist` directory, contains all required files. Ensure all files are included and modify the `.buildignore` file as needed. This ZIP file is your uploadable asset for the hub. -If you have never used typescript before, you can read about it here: https://www.typescriptlang.org/docs/ +## **Conclusion** -## **Distributing your mod:** +With this setup, you're ready to begin modding with SPT-AKI. If you run into any trouble be sure to check out the [modding documentation on the hub](https://hub.sp-tarkov.com/doc/lexicon/66-modding/). If you really get stuck feel free to join us in the [#mods-development](https://discord.com/channels/875684761291599922/875803116409323562) official Discord channel. -The project has been set up with an automatic task that will copy and zip ALL required files for your mod to work on SPT-AKI. -To run this task you just need to go to: - -> Terminal->Run Task...->Show All Tasks...->npm: build:zip - -The output will be a mod.zip file that will appear on the root of the project. - -Always verify that all files were included into the zip file. \ No newline at end of file +Build something awesome! diff --git a/TypeScript/15HttpListenerExample/build.mjs b/TypeScript/15HttpListenerExample/build.mjs new file mode 100644 index 0000000..d00117c --- /dev/null +++ b/TypeScript/15HttpListenerExample/build.mjs @@ -0,0 +1,383 @@ +#!/usr/bin/env node + +/** + * Build Script + * + * This script automates the build process for server-side SPT mod projects, facilitating the creation of distributable + * mod packages. It performs a series of operations as outlined below: + * - Loads the .buildignore file, which is used to list files that should be ignored during the build process. + * - Loads the package.json to get project details so a descriptive name can be created for the mod package. + * - Creates a distribution directory and a temporary working directory. + * - Copies files to the temporary directory while respecting the .buildignore rules. + * - Creates a zip archive of the project files. + * - Moves the zip file to the root of the distribution directory. + * - Cleans up the temporary directory. + * + * It's typical that this script be customized to suit the needs of each project. For example, the script can be updated + * to perform additional operations, such as moving the mod package to a specific location or uploading it to a server. + * This script is intended to be a starting point for developers to build upon. + * + * Usage: + * - Run this script using npm: `npm run build` + * - Use `npm run buildinfo` for detailed logging. + * + * Note: + * - Ensure that all necessary Node.js modules are installed before running the script: `npm install` + * - The script reads configurations from the `package.json` and `.buildignore` files; ensure they are correctly set up. + * + * @author Refringe + * @version v1.0.0 + */ + +import fs from "fs-extra"; +import os from "os"; +import path from "path"; +import { fileURLToPath } from "url"; +import { dirname } from "path"; +import ignore from "ignore"; +import archiver from "archiver"; +import winston from "winston"; + +// Get the command line arguments to determine whether to use verbose logging. +const args = process.argv.slice(2); +const verbose = args.includes("--verbose") || args.includes("-v"); + +// Configure the Winston logger to use colours. +const logColors = { + error: "red", + warn: "yellow", + info: "grey", + success: "green", +}; +winston.addColors(logColors); + +// Create a logger instance to log build progress. Configure the logger levels to allow for different levels of logging +// based on the verbosity flag, and set the console transport to log messages of the appropriate level. +const logger = winston.createLogger({ + levels: { + error: 0, + warn: 1, + success: 2, + info: 3, + }, + format: winston.format.combine( + winston.format.colorize(), + winston.format.printf(info => { + return `${info.level}: ${info.message}`; + }) + ), + transports: [ + new winston.transports.Console({ + level: verbose ? "info" : "success", + }), + ], +}); + +/** + * The main function orchestrates the build process for creating a distributable mod package. It leverages a series of + * helper functions to perform various tasks such as loading configuration files, setting up directories, copying files + * according to `.buildignore` rules, and creating a ZIP archive of the project files. + * + * Utilizes the Winston logger to provide information on the build status at different stages of the process. + * + * @returns {void} + */ +async function main() { + // Get the current directory where the script is being executed + const currentDir = getCurrentDirectory(); + + // Defining at this scope because we need to use it in the finally block. + let projectDir; + + try { + // Load the .buildignore file to set up an ignore handler for the build process. + const buildIgnorePatterns = await loadBuildIgnoreFile(currentDir); + + // Load the package.json file to get project details. + const packageJson = await loadPackageJson(currentDir); + + // Create a descriptive name for the mod package. + const projectName = createProjectName(packageJson); + logger.log("success", `Project name created: ${projectName}`); + + // Remove the old distribution directory and create a fresh one. + const distDir = await removeOldDistDirectory(currentDir); + logger.log("info", "Distribution directory successfully cleaned."); + + // Create a temporary working directory to perform the build operations. + projectDir = await createTemporaryDirectoryWithProjectName(projectName); + logger.log("success", "Temporary working directory successfully created."); + logger.log("info", projectDir); + + // Copy files to the temporary directory while respecting the .buildignore rules. + logger.log("info", "Beginning copy operation using .buildignore file..."); + await copyFiles(currentDir, projectDir, buildIgnorePatterns); + logger.log("success", "Files successfully copied to temporary directory."); + + // Create a zip archive of the project files. + logger.log("info", "Beginning folder compression..."); + const zipFilePath = path.join(path.dirname(projectDir), `${projectName}.zip`); + await createZipFile(projectDir, zipFilePath, "user/mods/" + projectName); + logger.log("success", "Archive successfully created."); + logger.log("info", zipFilePath); + + // Move the zip file inside of the project directory, within the temporary working directory. + const zipFileInProjectDir = path.join(projectDir, `${projectName}.zip`); + await fs.move(zipFilePath, zipFileInProjectDir); + logger.log("success", "Archive successfully moved."); + logger.log("info", zipFileInProjectDir); + + // Move the temporary directory into the distribution directory. + await fs.move(projectDir, distDir); + logger.log("success", "Temporary directory successfully moved into project distribution directory."); + + // Log the success message. Write out the path to the mod package. + logger.log("success", "------------------------------------"); + logger.log("success", "Build script completed successfully!"); + logger.log("success", "Your mod package has been created in the 'dist' directory:"); + logger.log("success", `/${path.relative(process.cwd(), path.join(distDir, `${projectName}.zip`))}`); + logger.log("success", "------------------------------------"); + if (!verbose) { + logger.log("success", "To see a detailed build log, use `npm run buildinfo`."); + logger.log("success", "------------------------------------"); + } + } catch (err) { + // If any of the file operations fail, log the error. + logger.log("error", "An error occurred: " + err); + } finally { + // Clean up the temporary directory, even if the build fails. + if (projectDir) { + try { + await fs.promises.rm(projectDir, { force: true, recursive: true }); + logger.log("info", "Cleaned temporary directory."); + } catch (err) { + logger.log("error", "Failed to clean temporary directory: " + err); + } + } + } +} + +/** + * Retrieves the current working directory where the script is being executed. This directory is used as a reference + * point for various file operations throughout the build process, ensuring that paths are resolved correctly regardless + * of the location from which the script is invoked. + * + * @returns {string} The absolute path of the current working directory. + */ +function getCurrentDirectory() { + return dirname(fileURLToPath(import.meta.url)); +} + +/** + * Loads the `.buildignore` file and sets up an ignore handler using the `ignore` module. The `.buildignore` file + * contains a list of patterns describing files and directories that should be ignored during the build process. The + * ignore handler created by this method is used to filter files and directories when copying them to the temporary + * directory, ensuring that only necessary files are included in the final mod package. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to an ignore handler. + */ +async function loadBuildIgnoreFile(currentDir) { + const buildIgnorePath = path.join(currentDir, ".buildignore"); + + try { + // Attempt to read the contents of the .buildignore file asynchronously. + const fileContent = await fs.promises.readFile(buildIgnorePath, "utf-8"); + + // Return a new ignore instance and add the rules from the .buildignore file (split by newlines). + return ignore().add(fileContent.split("\n")); + } catch (err) { + logger.log("warn", "Failed to read .buildignore file. No files or directories will be ignored."); + + // Return an empty ignore instance, ensuring the build process can continue. + return ignore(); + } +} + +/** + * Loads the `package.json` file and returns its content as a JSON object. The `package.json` file contains important + * project details such as the name and version, which are used in later stages of the build process to create a + * descriptive name for the mod package. The method reads the file from the current working directory, ensuring that it + * accurately reflects the current state of the project. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to a JSON object containing the contents of the `package.json`. + */ +async function loadPackageJson(currentDir) { + const packageJsonPath = path.join(currentDir, "package.json"); + + // Read the contents of the package.json file asynchronously as a UTF-8 string. + const packageJsonContent = await fs.promises.readFile(packageJsonPath, "utf-8"); + + return JSON.parse(packageJsonContent); +} + +/** + * Constructs a descriptive name for the mod package using details from the `package.json` file. The name is created by + * concatenating the project name, version, and a timestamp, resulting in a unique and descriptive file name for each + * build. This name is used as the base name for the temporary working directory and the final ZIP archive, helping to + * identify different versions of the mod package easily. + * + * @param {Object} packageJson - A JSON object containing the contents of the `package.json` file. + * @returns {string} A string representing the constructed project name. + */ +function createProjectName(packageJson) { + // Remove any non-alphanumeric characters from the author and name. + const author = packageJson.author.replace(/\W/g, ""); + const name = packageJson.name.replace(/\W/g, ""); + const version = packageJson.version; + + // Ensure the name is lowercase, as per the package.json specification. + return `${author}-${name}-${version}`.toLowerCase(); +} + +/** + * Defines the location of the distribution directory where the final mod package will be stored and deletes any + * existing distribution directory to ensure a clean slate for the build process. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to the absolute path to the distribution directory. + */ +async function removeOldDistDirectory(projectDir) { + const distPath = path.join(projectDir, "dist"); + await fs.remove(distPath); + return distPath; +} + +/** + * Creates a temporary working directory using the project name. This directory serves as a staging area where project + * files are gathered before being archived into the final mod package. The method constructs a unique directory path + * by appending the project name to a base temporary directory path, ensuring that each build has its own isolated + * working space. This approach facilitates clean and organized build processes, avoiding potential conflicts with other + * builds. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @param {string} projectName - The constructed project name, used to create a unique path for the temporary directory. + * @returns {Promise} A promise that resolves to the absolute path of the newly created temporary directory. + */ +async function createTemporaryDirectoryWithProjectName(projectName) { + // Create a new directory in the system's temporary folder to hold the project files. + const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "spt-mod-build-")); + + // Create a subdirectory within the temporary directory using the project name for this specific build. + const projectDir = path.join(tempDir, projectName); + await fs.ensureDir(projectDir); + + return projectDir; +} + +/** + * Copies the project files to the temporary directory while respecting the rules defined in the `.buildignore` file. + * The method is recursive, iterating over all files and directories in the source directory and using the ignore + * handler to filter out files and directories that match the patterns defined in the `.buildignore` file. This ensures + * that only the necessary files are included in the final mod package, adhering to the specifications defined by the + * developer in the `.buildignore` file. + * + * The copy operations are delayed and executed in parallel to improve efficiency and reduce the build time. This is + * achieved by creating an array of copy promises and awaiting them all at the end of the function. + * + * @param {string} sourceDirectory - The absolute path of the current working directory. + * @param {string} destinationDirectory - The absolute path of the temporary directory where the files will be copied. + * @param {Ignore} ignoreHandler - The ignore handler created from the `.buildignore` file. + * @returns {Promise} A promise that resolves when all copy operations are completed successfully. + */ +async function copyFiles(srcDir, destDir, ignoreHandler) { + try { + // Read the contents of the source directory to get a list of entries (files and directories). + const entries = await fs.promises.readdir(srcDir, { withFileTypes: true }); + + // Initialize an array to hold the promises returned by recursive calls to copyFiles and copyFile operations. + const copyOperations = []; + + for (const entry of entries) { + // Define the source and destination paths for each entry. + const srcPath = path.join(srcDir, entry.name); + const destPath = path.join(destDir, entry.name); + + // Get the relative path of the source file to check against the ignore handler. + const relativePath = path.relative(process.cwd(), srcPath); + + // If the ignore handler dictates that this file should be ignored, skip to the next iteration. + if (ignoreHandler.ignores(relativePath)) { + logger.log("info", `Ignored: /${path.relative(process.cwd(), srcPath)}`); + continue; + } + + if (entry.isDirectory()) { + // If the entry is a directory, create the corresponding temporary directory and make a recursive call + // to copyFiles to handle copying the contents of the directory. + await fs.ensureDir(destPath); + copyOperations.push(copyFiles(srcPath, destPath, ignoreHandler)); + } else { + // If the entry is a file, add a copyFile operation to the copyOperations array and log the event when + // the operation is successful. + copyOperations.push( + fs.copy(srcPath, destPath).then(() => { + logger.log("info", `Copied: /${path.relative(process.cwd(), srcPath)}`); + }) + ); + } + } + + // Await all copy operations to ensure all files and directories are copied before exiting the function. + await Promise.all(copyOperations); + } catch (err) { + // Log an error message if any error occurs during the copy process. + logger.log("error", "Error copying files: " + err); + } +} + +/** + * Creates a ZIP archive of the project files located in the temporary directory. The method uses the `archiver` module + * to create a ZIP file, which includes all the files that have been copied to the temporary directory during the build + * process. The ZIP file is named using the project name, helping to identify the contents of the archive easily. + * + * @param {string} directoryPath - The absolute path of the temporary directory containing the project files. + * @param {string} projectName - The constructed project name, used to name the ZIP file. + * @returns {Promise} A promise that resolves to the absolute path of the created ZIP file. + */ +async function createZipFile(directoryToZip, zipFilePath, containerDirName) { + return new Promise((resolve, reject) => { + // Create a write stream to the specified ZIP file path. + const output = fs.createWriteStream(zipFilePath); + + // Create a new archiver instance with ZIP format and maximum compression level. + const archive = archiver("zip", { + zlib: { level: 9 }, + }); + + // Set up an event listener for the 'close' event to resolve the promise when the archiver has finalized. + output.on("close", function () { + logger.log("info", "Archiver has finalized. The output and the file descriptor have closed."); + resolve(); + }); + + // Set up an event listener for the 'warning' event to handle warnings appropriately, logging them or rejecting + // the promise based on the error code. + archive.on("warning", function (err) { + if (err.code === "ENOENT") { + logger.log("warn", `Archiver issued a warning: ${err.code} - ${err.message}`); + } else { + reject(err); + } + }); + + // Set up an event listener for the 'error' event to reject the promise if any error occurs during archiving. + archive.on("error", function (err) { + reject(err); + }); + + // Pipe archive data to the file. + archive.pipe(output); + + // Add the directory to the archive, under the provided directory name. + archive.directory(directoryToZip, containerDirName); + + // Finalize the archive, indicating that no more files will be added and triggering the 'close' event once all + // data has been written. + archive.finalize(); + }); +} + +// Engage! +main(); diff --git a/TypeScript/15HttpListenerExample/package.json b/TypeScript/15HttpListenerExample/package.json index 69d17f5..e7e1b0f 100644 --- a/TypeScript/15HttpListenerExample/package.json +++ b/TypeScript/15HttpListenerExample/package.json @@ -7,17 +7,20 @@ "akiVersion": "~3.7", "scripts": { "setup": "npm i", - "build": "node ./packageBuild.ts" + "build": "node ./build.mjs", + "buildinfo": "node ./build.mjs --verbose" }, "devDependencies": { "@types/node": "20.4.5", "@typescript-eslint/eslint-plugin": "6.2.0", "@typescript-eslint/parser": "6.2.0", - "bestzip": "2.2.1", + "archiver": "^6.0", "eslint": "8.46.0", - "fs-extra": "11.1.1", - "glob": "10.3.3", + "fs-extra": "^11.1", + "ignore": "^5.2", + "os": "^0.1", "tsyringe": "4.8.0", - "typescript": "5.1.6" + "typescript": "5.1.6", + "winston": "^3.9" } -} \ No newline at end of file +} diff --git a/TypeScript/15HttpListenerExample/packageBuild.ts b/TypeScript/15HttpListenerExample/packageBuild.ts deleted file mode 100644 index d76ddd8..0000000 --- a/TypeScript/15HttpListenerExample/packageBuild.ts +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env node - -// This is a simple script used to build a mod package. The script will copy necessary files to the build directory -// and compress the build directory into a zip file that can be easily shared. - -const fs = require("fs-extra"); -const glob = require("glob"); -const zip = require('bestzip'); -const path = require("path"); -const minimatch = require('minimatch'); - -// Load the package.json file to get some information about the package so we can name things appropriately. This is -// atypical, and you would never do this in a production environment, but this script is only used for development so -// it's fine in this case. Some of these values are stored in environment variables, but those differ between node -// versions; the 'author' value is not available after node v14. -const { author, name:packageName, version } = require("./package.json"); - -// Generate the name of the package, stripping out all non-alphanumeric characters in the 'author' and 'name'. -const modName = `${author.replace(/[^a-z0-9]/gi, "")}-${packageName.replace(/[^a-z0-9]/gi, "")}-${version}`; -console.log(`Generated package name: ${modName}`); - -// Delete the old build directory and compressed package file. -fs.rmSync(`${__dirname}/dist`, { force: true, recursive: true }); -console.log("Previous build files deleted."); - -// Generate a list of files that should not be copied over into the distribution directory. This is a blacklist to ensure -// we always copy over additional files and directories that authors may have added to their project. This may need to be -// expanded upon by the mod author to allow for node modules that are used within the mod; example commented out below. -const ignoreList = [ - "node_modules/", - // "node_modules/!(weighted|glob)", // Instead of excluding the entire node_modules directory, allow two node modules. - "src/**/*.js", - "types/", - ".git/", - ".gitea/", - ".eslintignore", - ".eslintrc.json", - ".gitignore", - ".DS_Store", - "packageBuild.ts", - "mod.code-workspace", - "package-lock.json", - "tsconfig.json" -]; -const exclude = glob.sync(`{${ignoreList.join(",")}}`, { realpath: true, dot: true }); - -fs.copySync(__dirname, path.normalize(`${__dirname}/../~${modName}`), {filter: (src) => -{ - const relativePath = path.relative(__dirname, src); - const shouldExclude = exclude.some((pattern) => minimatch(relativePath, pattern)); - console.log(`${relativePath} - Excluded: ${shouldExclude}`); - return !shouldExclude; -},}); - -fs.moveSync(path.normalize(`${__dirname}/../~${modName}`), path.normalize(`${__dirname}/${modName}`), { overwrite: true }); -fs.copySync(path.normalize(`${__dirname}/${modName}`), path.normalize(`${__dirname}/dist`)); -console.log("Build files copied."); - -// Compress the files for easy distribution. The compressed file is saved into the dist directory. When uncompressed we -// need to be sure that it includes a directory that the user can easily copy into their game mods directory. -zip({ - source: modName, - destination: `dist/${modName}.zip`, - cwd: __dirname -}).catch(function(err) -{ - console.error("A bestzip error has occurred: ", err.stack); -}).then(function() -{ - console.log(`Compressed mod package to: /dist/${modName}.zip`); - - // Now that we're done with the compression we can delete the temporary build directory. - fs.rmSync(`${__dirname}/${modName}`, { force: true, recursive: true }); - console.log("Build successful! your zip file has been created and is ready to be uploaded to hub.sp-tarkov.com/files/"); -}); \ No newline at end of file diff --git a/TypeScript/16ImporterUtil/.buildignore b/TypeScript/16ImporterUtil/.buildignore new file mode 100644 index 0000000..2cbde65 --- /dev/null +++ b/TypeScript/16ImporterUtil/.buildignore @@ -0,0 +1,20 @@ +/.buildignore +/.DS_Store +/.editorconfig +/.eslintignore +/.eslintrc.json +/.git +/.github +/.gitignore +/.gitlab +/.nvmrc +/.prettierrc +/.vscode +/build.mjs +/dist +/images +/mod.code-workspace +/node_modules +/package-lock.json +/tsconfig.json +/types diff --git a/TypeScript/16ImporterUtil/.eslintrc.json b/TypeScript/16ImporterUtil/.eslintrc.json index 98e7a0c..071a313 100644 --- a/TypeScript/16ImporterUtil/.eslintrc.json +++ b/TypeScript/16ImporterUtil/.eslintrc.json @@ -1,81 +1,98 @@ { - "root": true, - "parser": "@typescript-eslint/parser", - "plugins": [ - "@typescript-eslint" - ], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended" - ], - "rules": { - "max-len": [ - "warn", - { - "code":100 - } + "root": true, + "parser": "@typescript-eslint/parser", + "plugins": [ + "@typescript-eslint" ], - "@typescript-eslint/no-explicit-any": 0, - "@typescript-eslint/no-unused-vars": 1, - "@typescript-eslint/no-empty-interface": 0, - "@typescript-eslint/no-namespace": 0, - "@typescript-eslint/comma-dangle": 1, - "@typescript-eslint/func-call-spacing": 2, - "@typescript-eslint/quotes": 1, - "@typescript-eslint/brace-style": [ - "warn", - "allman" + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" ], - "@typescript-eslint/naming-convention": [ - "warn", - { - "selector": "default", - "format": ["camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "typeLike", - "format": ["PascalCase"] - }, - { - "selector": "objectLiteralProperty", - "format": ["PascalCase", "camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "typeProperty", - "format": ["PascalCase", "camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "enumMember", - "format": ["UPPER_CASE"] - } - ], - "@typescript-eslint/indent": [ - "warn", - 4 - ], - "@typescript-eslint/no-unused-expressions": [ - "warn", - { - "allowShortCircuit": false, - "allowTernary": false - } - ], - "@typescript-eslint/keyword-spacing": [ - "warn", - { - "before": true, - "after": true - } - ], - "@typescript-eslint/explicit-module-boundary-types": [ - "warn", - { - "allowArgumentsExplicitlyTypedAsAny": true - } + "rules": { + "@typescript-eslint/no-explicit-any": 0, + "@typescript-eslint/no-unused-vars": 1, + "@typescript-eslint/no-empty-interface": 0, + "@typescript-eslint/no-namespace": 0, + "@typescript-eslint/comma-dangle": 1, + "@typescript-eslint/func-call-spacing": 2, + "@typescript-eslint/quotes": 1, + "@typescript-eslint/brace-style": [ + "warn", + "allman" + ], + "@typescript-eslint/naming-convention": [ + "warn", + { + "selector": "default", + "format": [ + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "typeLike", + "format": [ + "PascalCase" + ] + }, + { + "selector": "objectLiteralProperty", + "format": [ + "PascalCase", + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "typeProperty", + "format": [ + "PascalCase", + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "enumMember", + "format": [ + "UPPER_CASE" + ] + } + ], + "@typescript-eslint/indent": [ + "warn", + 4 + ], + "@typescript-eslint/no-unused-expressions": [ + "warn", + { + "allowShortCircuit": false, + "allowTernary": false + } + ], + "@typescript-eslint/keyword-spacing": [ + "warn", + { + "before": true, + "after": true + } + ], + "@typescript-eslint/explicit-module-boundary-types": [ + "warn", + { + "allowArgumentsExplicitlyTypedAsAny": true + } + ] + }, + "overrides": [ + { + "files": [ + "*.mjs", + "*.ts" + ], + "env": { + "node": true + } + } ] - } } \ No newline at end of file diff --git a/TypeScript/16ImporterUtil/README.md b/TypeScript/16ImporterUtil/README.md index 434d063..8cba28e 100644 --- a/TypeScript/16ImporterUtil/README.md +++ b/TypeScript/16ImporterUtil/README.md @@ -1,64 +1,66 @@ +# Welcome to the SPT-AKI Modding Project -This project was created to automate most parts of building and setting up an environment. +This project is designed to streamline the initial setup process for building and creating mods in the SPT-AKI environment. Follow this guide to set up your environment efficiently. -## **NodeJS:** +## **Table of Contents** +- [NodeJS Setup](#nodejs-setup) +- [IDE Setup](#ide-setup) +- [Workspace Configuration](#workspace-configuration) +- [Environment Setup](#environment-setup) +- [Essential Concepts](#essential-concepts) +- [Coding Guidelines](#coding-guidelines) +- [Distribution Guidelines](#distribution-guidelines) -The first step would be to install nodejs on your pc, the version you NEED is **16.17.1** +## **NodeJS Setup** -That version is the one that has been used to test the mod templates and build scripts. +Before you begin, ensure to install NodeJS version `v16.17.1`, which has been tested thoroughly with our mod templates and build scripts. Download it from the [official NodeJS website](https://nodejs.org/). -It can be downloaded from here: https://nodejs.org/dist/v16.17.1/node-v16.17.1-x64.msi +After installation, it's advised to reboot your system. -A system reboot may be needed after install. +## **IDE Setup** -## **IDE:** +For this project, you can work with either [VSCodium](https://vscodium.com/) or [VSCode](https://code.visualstudio.com/). However, we strongly recommend using VSCode, as all development and testing have been carried out using this IDE, ensuring a smoother experience and compatibility with the project setups. Either way, we have a prepared a workspace file to assist you in setting up your environment. -The second step is having an IDE ready. We've setup a VSCodium workspace file to help with this. +## **Workspace Configuration** -You CAN use Visual Studio Code if you so desire, just keep in mind that our devs tests on the mod files was done using VSCode. +With NodeJS and your chosen IDE ready, initiate the `mod.code-workspace` file using your IDE: -You can get VSCode here: https://code.visualstudio.com/ +> File -> Open Workspace from File... -## **Workspace:** +Upon project loading, consider installing recommended plugins like the ESLint plugin. -Once you have NodeJS and VSCode/VSCodium ready, open the mod.code-workspace file with VSCode (File->Open Workspace from File...). +## **Environment Setup** -Once the project loads you may be recommended to install the ESLint plugin. This is HIGHLY recommended. +An automated task is available to configure your environment for Typescript utilization: -## **Environment Setup:** +> Terminal -> Run Task... -> Show All Tasks... -> npm: install -There is a task that will automatically setup your environment to use typescript. +Note: Preserve the `node_modules` folder as it contains necessary dependencies for Typescript and other functionalities. -To run it, you just need to go to: +## **Essential Concepts** -> Terminal->Run Task...->Show All Tasks...->npm: install +Prioritize understanding Dependency Injection and Inversion of Control, the architectural principles SPT-AKI adopts. Comprehensive guidelines will be available on the hub upon release. -After running this task, your environment will be ready to start coding. +Some resources to get you started: + - [A quick intro to Dependency Injection](https://www.freecodecamp.org/news/a-quick-intro-to-dependency-injection-what-it-is-and-when-to-use-it-7578c84fa88f/) + - [Understanding Inversion of Control (IoC) Principle](https://medium.com/@amitkma/understanding-inversion-of-control-ioc-principle-163b1dc97454) -DO NOT remove the node_modules folder, this is an auto generated directory that has the required dependencies to be able to use typescript and more. +## **Coding Guidelines** -## **IMPORTANT:** +Focus your mod development around the `mod.ts` file. In the `package.json` file, only alter these properties: `"name"`, `"version"`, `"license"`, `"author"`, and `"akiVersion"`. -Before starting to work on your mod, we suggest you read about Dependency Injection and Inversion of Control as this is the adopted architecture SPT-AKI has adopted. +New to Typescript? Find comprehensive documentation on the [official website](https://www.typescriptlang.org/docs/). -It will be difficult to understand some of the problems you may be having if you dont understand the basics of it. +## **Distribution Guidelines** -A guide explaining all the essentials will be available on the hub on release for you to read about. +Automated tasks are set up to bundle all necessary files for your mod to function in SPT-AKI: -## **Coding:** +> Terminal -> Run Task... -> Show All Tasks... -> npm: build -All your work should be centered around the mod.ts file as an entry point. -You can ONLY change the following properties from the package.json file: `"name"`, `"version"`, `"license"`: `"MIT"`, `"author"`, `"akiVersion"`. +The ZIP output, located in the `dist` directory, contains all required files. Ensure all files are included and modify the `.buildignore` file as needed. This ZIP file is your uploadable asset for the hub. -If you have never used typescript before, you can read about it here: https://www.typescriptlang.org/docs/ +## **Conclusion** -## **Distributing your mod:** +With this setup, you're ready to begin modding with SPT-AKI. If you run into any trouble be sure to check out the [modding documentation on the hub](https://hub.sp-tarkov.com/doc/lexicon/66-modding/). If you really get stuck feel free to join us in the [#mods-development](https://discord.com/channels/875684761291599922/875803116409323562) official Discord channel. -The project has been set up with an automatic task that will copy and zip ALL required files for your mod to work on SPT-AKI. -To run this task you just need to go to: - -> Terminal->Run Task...->Show All Tasks...->npm: build:zip - -The output will be a mod.zip file that will appear on the root of the project. - -Always verify that all files were included into the zip file. \ No newline at end of file +Build something awesome! diff --git a/TypeScript/16ImporterUtil/build.mjs b/TypeScript/16ImporterUtil/build.mjs new file mode 100644 index 0000000..d00117c --- /dev/null +++ b/TypeScript/16ImporterUtil/build.mjs @@ -0,0 +1,383 @@ +#!/usr/bin/env node + +/** + * Build Script + * + * This script automates the build process for server-side SPT mod projects, facilitating the creation of distributable + * mod packages. It performs a series of operations as outlined below: + * - Loads the .buildignore file, which is used to list files that should be ignored during the build process. + * - Loads the package.json to get project details so a descriptive name can be created for the mod package. + * - Creates a distribution directory and a temporary working directory. + * - Copies files to the temporary directory while respecting the .buildignore rules. + * - Creates a zip archive of the project files. + * - Moves the zip file to the root of the distribution directory. + * - Cleans up the temporary directory. + * + * It's typical that this script be customized to suit the needs of each project. For example, the script can be updated + * to perform additional operations, such as moving the mod package to a specific location or uploading it to a server. + * This script is intended to be a starting point for developers to build upon. + * + * Usage: + * - Run this script using npm: `npm run build` + * - Use `npm run buildinfo` for detailed logging. + * + * Note: + * - Ensure that all necessary Node.js modules are installed before running the script: `npm install` + * - The script reads configurations from the `package.json` and `.buildignore` files; ensure they are correctly set up. + * + * @author Refringe + * @version v1.0.0 + */ + +import fs from "fs-extra"; +import os from "os"; +import path from "path"; +import { fileURLToPath } from "url"; +import { dirname } from "path"; +import ignore from "ignore"; +import archiver from "archiver"; +import winston from "winston"; + +// Get the command line arguments to determine whether to use verbose logging. +const args = process.argv.slice(2); +const verbose = args.includes("--verbose") || args.includes("-v"); + +// Configure the Winston logger to use colours. +const logColors = { + error: "red", + warn: "yellow", + info: "grey", + success: "green", +}; +winston.addColors(logColors); + +// Create a logger instance to log build progress. Configure the logger levels to allow for different levels of logging +// based on the verbosity flag, and set the console transport to log messages of the appropriate level. +const logger = winston.createLogger({ + levels: { + error: 0, + warn: 1, + success: 2, + info: 3, + }, + format: winston.format.combine( + winston.format.colorize(), + winston.format.printf(info => { + return `${info.level}: ${info.message}`; + }) + ), + transports: [ + new winston.transports.Console({ + level: verbose ? "info" : "success", + }), + ], +}); + +/** + * The main function orchestrates the build process for creating a distributable mod package. It leverages a series of + * helper functions to perform various tasks such as loading configuration files, setting up directories, copying files + * according to `.buildignore` rules, and creating a ZIP archive of the project files. + * + * Utilizes the Winston logger to provide information on the build status at different stages of the process. + * + * @returns {void} + */ +async function main() { + // Get the current directory where the script is being executed + const currentDir = getCurrentDirectory(); + + // Defining at this scope because we need to use it in the finally block. + let projectDir; + + try { + // Load the .buildignore file to set up an ignore handler for the build process. + const buildIgnorePatterns = await loadBuildIgnoreFile(currentDir); + + // Load the package.json file to get project details. + const packageJson = await loadPackageJson(currentDir); + + // Create a descriptive name for the mod package. + const projectName = createProjectName(packageJson); + logger.log("success", `Project name created: ${projectName}`); + + // Remove the old distribution directory and create a fresh one. + const distDir = await removeOldDistDirectory(currentDir); + logger.log("info", "Distribution directory successfully cleaned."); + + // Create a temporary working directory to perform the build operations. + projectDir = await createTemporaryDirectoryWithProjectName(projectName); + logger.log("success", "Temporary working directory successfully created."); + logger.log("info", projectDir); + + // Copy files to the temporary directory while respecting the .buildignore rules. + logger.log("info", "Beginning copy operation using .buildignore file..."); + await copyFiles(currentDir, projectDir, buildIgnorePatterns); + logger.log("success", "Files successfully copied to temporary directory."); + + // Create a zip archive of the project files. + logger.log("info", "Beginning folder compression..."); + const zipFilePath = path.join(path.dirname(projectDir), `${projectName}.zip`); + await createZipFile(projectDir, zipFilePath, "user/mods/" + projectName); + logger.log("success", "Archive successfully created."); + logger.log("info", zipFilePath); + + // Move the zip file inside of the project directory, within the temporary working directory. + const zipFileInProjectDir = path.join(projectDir, `${projectName}.zip`); + await fs.move(zipFilePath, zipFileInProjectDir); + logger.log("success", "Archive successfully moved."); + logger.log("info", zipFileInProjectDir); + + // Move the temporary directory into the distribution directory. + await fs.move(projectDir, distDir); + logger.log("success", "Temporary directory successfully moved into project distribution directory."); + + // Log the success message. Write out the path to the mod package. + logger.log("success", "------------------------------------"); + logger.log("success", "Build script completed successfully!"); + logger.log("success", "Your mod package has been created in the 'dist' directory:"); + logger.log("success", `/${path.relative(process.cwd(), path.join(distDir, `${projectName}.zip`))}`); + logger.log("success", "------------------------------------"); + if (!verbose) { + logger.log("success", "To see a detailed build log, use `npm run buildinfo`."); + logger.log("success", "------------------------------------"); + } + } catch (err) { + // If any of the file operations fail, log the error. + logger.log("error", "An error occurred: " + err); + } finally { + // Clean up the temporary directory, even if the build fails. + if (projectDir) { + try { + await fs.promises.rm(projectDir, { force: true, recursive: true }); + logger.log("info", "Cleaned temporary directory."); + } catch (err) { + logger.log("error", "Failed to clean temporary directory: " + err); + } + } + } +} + +/** + * Retrieves the current working directory where the script is being executed. This directory is used as a reference + * point for various file operations throughout the build process, ensuring that paths are resolved correctly regardless + * of the location from which the script is invoked. + * + * @returns {string} The absolute path of the current working directory. + */ +function getCurrentDirectory() { + return dirname(fileURLToPath(import.meta.url)); +} + +/** + * Loads the `.buildignore` file and sets up an ignore handler using the `ignore` module. The `.buildignore` file + * contains a list of patterns describing files and directories that should be ignored during the build process. The + * ignore handler created by this method is used to filter files and directories when copying them to the temporary + * directory, ensuring that only necessary files are included in the final mod package. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to an ignore handler. + */ +async function loadBuildIgnoreFile(currentDir) { + const buildIgnorePath = path.join(currentDir, ".buildignore"); + + try { + // Attempt to read the contents of the .buildignore file asynchronously. + const fileContent = await fs.promises.readFile(buildIgnorePath, "utf-8"); + + // Return a new ignore instance and add the rules from the .buildignore file (split by newlines). + return ignore().add(fileContent.split("\n")); + } catch (err) { + logger.log("warn", "Failed to read .buildignore file. No files or directories will be ignored."); + + // Return an empty ignore instance, ensuring the build process can continue. + return ignore(); + } +} + +/** + * Loads the `package.json` file and returns its content as a JSON object. The `package.json` file contains important + * project details such as the name and version, which are used in later stages of the build process to create a + * descriptive name for the mod package. The method reads the file from the current working directory, ensuring that it + * accurately reflects the current state of the project. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to a JSON object containing the contents of the `package.json`. + */ +async function loadPackageJson(currentDir) { + const packageJsonPath = path.join(currentDir, "package.json"); + + // Read the contents of the package.json file asynchronously as a UTF-8 string. + const packageJsonContent = await fs.promises.readFile(packageJsonPath, "utf-8"); + + return JSON.parse(packageJsonContent); +} + +/** + * Constructs a descriptive name for the mod package using details from the `package.json` file. The name is created by + * concatenating the project name, version, and a timestamp, resulting in a unique and descriptive file name for each + * build. This name is used as the base name for the temporary working directory and the final ZIP archive, helping to + * identify different versions of the mod package easily. + * + * @param {Object} packageJson - A JSON object containing the contents of the `package.json` file. + * @returns {string} A string representing the constructed project name. + */ +function createProjectName(packageJson) { + // Remove any non-alphanumeric characters from the author and name. + const author = packageJson.author.replace(/\W/g, ""); + const name = packageJson.name.replace(/\W/g, ""); + const version = packageJson.version; + + // Ensure the name is lowercase, as per the package.json specification. + return `${author}-${name}-${version}`.toLowerCase(); +} + +/** + * Defines the location of the distribution directory where the final mod package will be stored and deletes any + * existing distribution directory to ensure a clean slate for the build process. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to the absolute path to the distribution directory. + */ +async function removeOldDistDirectory(projectDir) { + const distPath = path.join(projectDir, "dist"); + await fs.remove(distPath); + return distPath; +} + +/** + * Creates a temporary working directory using the project name. This directory serves as a staging area where project + * files are gathered before being archived into the final mod package. The method constructs a unique directory path + * by appending the project name to a base temporary directory path, ensuring that each build has its own isolated + * working space. This approach facilitates clean and organized build processes, avoiding potential conflicts with other + * builds. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @param {string} projectName - The constructed project name, used to create a unique path for the temporary directory. + * @returns {Promise} A promise that resolves to the absolute path of the newly created temporary directory. + */ +async function createTemporaryDirectoryWithProjectName(projectName) { + // Create a new directory in the system's temporary folder to hold the project files. + const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "spt-mod-build-")); + + // Create a subdirectory within the temporary directory using the project name for this specific build. + const projectDir = path.join(tempDir, projectName); + await fs.ensureDir(projectDir); + + return projectDir; +} + +/** + * Copies the project files to the temporary directory while respecting the rules defined in the `.buildignore` file. + * The method is recursive, iterating over all files and directories in the source directory and using the ignore + * handler to filter out files and directories that match the patterns defined in the `.buildignore` file. This ensures + * that only the necessary files are included in the final mod package, adhering to the specifications defined by the + * developer in the `.buildignore` file. + * + * The copy operations are delayed and executed in parallel to improve efficiency and reduce the build time. This is + * achieved by creating an array of copy promises and awaiting them all at the end of the function. + * + * @param {string} sourceDirectory - The absolute path of the current working directory. + * @param {string} destinationDirectory - The absolute path of the temporary directory where the files will be copied. + * @param {Ignore} ignoreHandler - The ignore handler created from the `.buildignore` file. + * @returns {Promise} A promise that resolves when all copy operations are completed successfully. + */ +async function copyFiles(srcDir, destDir, ignoreHandler) { + try { + // Read the contents of the source directory to get a list of entries (files and directories). + const entries = await fs.promises.readdir(srcDir, { withFileTypes: true }); + + // Initialize an array to hold the promises returned by recursive calls to copyFiles and copyFile operations. + const copyOperations = []; + + for (const entry of entries) { + // Define the source and destination paths for each entry. + const srcPath = path.join(srcDir, entry.name); + const destPath = path.join(destDir, entry.name); + + // Get the relative path of the source file to check against the ignore handler. + const relativePath = path.relative(process.cwd(), srcPath); + + // If the ignore handler dictates that this file should be ignored, skip to the next iteration. + if (ignoreHandler.ignores(relativePath)) { + logger.log("info", `Ignored: /${path.relative(process.cwd(), srcPath)}`); + continue; + } + + if (entry.isDirectory()) { + // If the entry is a directory, create the corresponding temporary directory and make a recursive call + // to copyFiles to handle copying the contents of the directory. + await fs.ensureDir(destPath); + copyOperations.push(copyFiles(srcPath, destPath, ignoreHandler)); + } else { + // If the entry is a file, add a copyFile operation to the copyOperations array and log the event when + // the operation is successful. + copyOperations.push( + fs.copy(srcPath, destPath).then(() => { + logger.log("info", `Copied: /${path.relative(process.cwd(), srcPath)}`); + }) + ); + } + } + + // Await all copy operations to ensure all files and directories are copied before exiting the function. + await Promise.all(copyOperations); + } catch (err) { + // Log an error message if any error occurs during the copy process. + logger.log("error", "Error copying files: " + err); + } +} + +/** + * Creates a ZIP archive of the project files located in the temporary directory. The method uses the `archiver` module + * to create a ZIP file, which includes all the files that have been copied to the temporary directory during the build + * process. The ZIP file is named using the project name, helping to identify the contents of the archive easily. + * + * @param {string} directoryPath - The absolute path of the temporary directory containing the project files. + * @param {string} projectName - The constructed project name, used to name the ZIP file. + * @returns {Promise} A promise that resolves to the absolute path of the created ZIP file. + */ +async function createZipFile(directoryToZip, zipFilePath, containerDirName) { + return new Promise((resolve, reject) => { + // Create a write stream to the specified ZIP file path. + const output = fs.createWriteStream(zipFilePath); + + // Create a new archiver instance with ZIP format and maximum compression level. + const archive = archiver("zip", { + zlib: { level: 9 }, + }); + + // Set up an event listener for the 'close' event to resolve the promise when the archiver has finalized. + output.on("close", function () { + logger.log("info", "Archiver has finalized. The output and the file descriptor have closed."); + resolve(); + }); + + // Set up an event listener for the 'warning' event to handle warnings appropriately, logging them or rejecting + // the promise based on the error code. + archive.on("warning", function (err) { + if (err.code === "ENOENT") { + logger.log("warn", `Archiver issued a warning: ${err.code} - ${err.message}`); + } else { + reject(err); + } + }); + + // Set up an event listener for the 'error' event to reject the promise if any error occurs during archiving. + archive.on("error", function (err) { + reject(err); + }); + + // Pipe archive data to the file. + archive.pipe(output); + + // Add the directory to the archive, under the provided directory name. + archive.directory(directoryToZip, containerDirName); + + // Finalize the archive, indicating that no more files will be added and triggering the 'close' event once all + // data has been written. + archive.finalize(); + }); +} + +// Engage! +main(); diff --git a/TypeScript/16ImporterUtil/package.json b/TypeScript/16ImporterUtil/package.json index 11200aa..ea78595 100644 --- a/TypeScript/16ImporterUtil/package.json +++ b/TypeScript/16ImporterUtil/package.json @@ -7,17 +7,20 @@ "akiVersion": "~3.7", "scripts": { "setup": "npm i", - "build": "node ./packageBuild.ts" + "build": "node ./build.mjs", + "buildinfo": "node ./build.mjs --verbose" }, "devDependencies": { "@types/node": "20.4.5", "@typescript-eslint/eslint-plugin": "6.2.0", "@typescript-eslint/parser": "6.2.0", - "bestzip": "2.2.1", + "archiver": "^6.0", "eslint": "8.46.0", - "fs-extra": "11.1.1", - "glob": "10.3.3", + "fs-extra": "^11.1", + "ignore": "^5.2", + "os": "^0.1", "tsyringe": "4.8.0", - "typescript": "5.1.6" + "typescript": "5.1.6", + "winston": "^3.9" } -} \ No newline at end of file +} diff --git a/TypeScript/16ImporterUtil/packageBuild.ts b/TypeScript/16ImporterUtil/packageBuild.ts deleted file mode 100644 index d76ddd8..0000000 --- a/TypeScript/16ImporterUtil/packageBuild.ts +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env node - -// This is a simple script used to build a mod package. The script will copy necessary files to the build directory -// and compress the build directory into a zip file that can be easily shared. - -const fs = require("fs-extra"); -const glob = require("glob"); -const zip = require('bestzip'); -const path = require("path"); -const minimatch = require('minimatch'); - -// Load the package.json file to get some information about the package so we can name things appropriately. This is -// atypical, and you would never do this in a production environment, but this script is only used for development so -// it's fine in this case. Some of these values are stored in environment variables, but those differ between node -// versions; the 'author' value is not available after node v14. -const { author, name:packageName, version } = require("./package.json"); - -// Generate the name of the package, stripping out all non-alphanumeric characters in the 'author' and 'name'. -const modName = `${author.replace(/[^a-z0-9]/gi, "")}-${packageName.replace(/[^a-z0-9]/gi, "")}-${version}`; -console.log(`Generated package name: ${modName}`); - -// Delete the old build directory and compressed package file. -fs.rmSync(`${__dirname}/dist`, { force: true, recursive: true }); -console.log("Previous build files deleted."); - -// Generate a list of files that should not be copied over into the distribution directory. This is a blacklist to ensure -// we always copy over additional files and directories that authors may have added to their project. This may need to be -// expanded upon by the mod author to allow for node modules that are used within the mod; example commented out below. -const ignoreList = [ - "node_modules/", - // "node_modules/!(weighted|glob)", // Instead of excluding the entire node_modules directory, allow two node modules. - "src/**/*.js", - "types/", - ".git/", - ".gitea/", - ".eslintignore", - ".eslintrc.json", - ".gitignore", - ".DS_Store", - "packageBuild.ts", - "mod.code-workspace", - "package-lock.json", - "tsconfig.json" -]; -const exclude = glob.sync(`{${ignoreList.join(",")}}`, { realpath: true, dot: true }); - -fs.copySync(__dirname, path.normalize(`${__dirname}/../~${modName}`), {filter: (src) => -{ - const relativePath = path.relative(__dirname, src); - const shouldExclude = exclude.some((pattern) => minimatch(relativePath, pattern)); - console.log(`${relativePath} - Excluded: ${shouldExclude}`); - return !shouldExclude; -},}); - -fs.moveSync(path.normalize(`${__dirname}/../~${modName}`), path.normalize(`${__dirname}/${modName}`), { overwrite: true }); -fs.copySync(path.normalize(`${__dirname}/${modName}`), path.normalize(`${__dirname}/dist`)); -console.log("Build files copied."); - -// Compress the files for easy distribution. The compressed file is saved into the dist directory. When uncompressed we -// need to be sure that it includes a directory that the user can easily copy into their game mods directory. -zip({ - source: modName, - destination: `dist/${modName}.zip`, - cwd: __dirname -}).catch(function(err) -{ - console.error("A bestzip error has occurred: ", err.stack); -}).then(function() -{ - console.log(`Compressed mod package to: /dist/${modName}.zip`); - - // Now that we're done with the compression we can delete the temporary build directory. - fs.rmSync(`${__dirname}/${modName}`, { force: true, recursive: true }); - console.log("Build successful! your zip file has been created and is ready to be uploaded to hub.sp-tarkov.com/files/"); -}); \ No newline at end of file diff --git a/TypeScript/17AsyncImporterWithDependency1/.buildignore b/TypeScript/17AsyncImporterWithDependency1/.buildignore new file mode 100644 index 0000000..2cbde65 --- /dev/null +++ b/TypeScript/17AsyncImporterWithDependency1/.buildignore @@ -0,0 +1,20 @@ +/.buildignore +/.DS_Store +/.editorconfig +/.eslintignore +/.eslintrc.json +/.git +/.github +/.gitignore +/.gitlab +/.nvmrc +/.prettierrc +/.vscode +/build.mjs +/dist +/images +/mod.code-workspace +/node_modules +/package-lock.json +/tsconfig.json +/types diff --git a/TypeScript/17AsyncImporterWithDependency1/.eslintrc.json b/TypeScript/17AsyncImporterWithDependency1/.eslintrc.json index 98e7a0c..071a313 100644 --- a/TypeScript/17AsyncImporterWithDependency1/.eslintrc.json +++ b/TypeScript/17AsyncImporterWithDependency1/.eslintrc.json @@ -1,81 +1,98 @@ { - "root": true, - "parser": "@typescript-eslint/parser", - "plugins": [ - "@typescript-eslint" - ], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended" - ], - "rules": { - "max-len": [ - "warn", - { - "code":100 - } + "root": true, + "parser": "@typescript-eslint/parser", + "plugins": [ + "@typescript-eslint" ], - "@typescript-eslint/no-explicit-any": 0, - "@typescript-eslint/no-unused-vars": 1, - "@typescript-eslint/no-empty-interface": 0, - "@typescript-eslint/no-namespace": 0, - "@typescript-eslint/comma-dangle": 1, - "@typescript-eslint/func-call-spacing": 2, - "@typescript-eslint/quotes": 1, - "@typescript-eslint/brace-style": [ - "warn", - "allman" + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" ], - "@typescript-eslint/naming-convention": [ - "warn", - { - "selector": "default", - "format": ["camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "typeLike", - "format": ["PascalCase"] - }, - { - "selector": "objectLiteralProperty", - "format": ["PascalCase", "camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "typeProperty", - "format": ["PascalCase", "camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "enumMember", - "format": ["UPPER_CASE"] - } - ], - "@typescript-eslint/indent": [ - "warn", - 4 - ], - "@typescript-eslint/no-unused-expressions": [ - "warn", - { - "allowShortCircuit": false, - "allowTernary": false - } - ], - "@typescript-eslint/keyword-spacing": [ - "warn", - { - "before": true, - "after": true - } - ], - "@typescript-eslint/explicit-module-boundary-types": [ - "warn", - { - "allowArgumentsExplicitlyTypedAsAny": true - } + "rules": { + "@typescript-eslint/no-explicit-any": 0, + "@typescript-eslint/no-unused-vars": 1, + "@typescript-eslint/no-empty-interface": 0, + "@typescript-eslint/no-namespace": 0, + "@typescript-eslint/comma-dangle": 1, + "@typescript-eslint/func-call-spacing": 2, + "@typescript-eslint/quotes": 1, + "@typescript-eslint/brace-style": [ + "warn", + "allman" + ], + "@typescript-eslint/naming-convention": [ + "warn", + { + "selector": "default", + "format": [ + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "typeLike", + "format": [ + "PascalCase" + ] + }, + { + "selector": "objectLiteralProperty", + "format": [ + "PascalCase", + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "typeProperty", + "format": [ + "PascalCase", + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "enumMember", + "format": [ + "UPPER_CASE" + ] + } + ], + "@typescript-eslint/indent": [ + "warn", + 4 + ], + "@typescript-eslint/no-unused-expressions": [ + "warn", + { + "allowShortCircuit": false, + "allowTernary": false + } + ], + "@typescript-eslint/keyword-spacing": [ + "warn", + { + "before": true, + "after": true + } + ], + "@typescript-eslint/explicit-module-boundary-types": [ + "warn", + { + "allowArgumentsExplicitlyTypedAsAny": true + } + ] + }, + "overrides": [ + { + "files": [ + "*.mjs", + "*.ts" + ], + "env": { + "node": true + } + } ] - } } \ No newline at end of file diff --git a/TypeScript/17AsyncImporterWithDependency1/README.md b/TypeScript/17AsyncImporterWithDependency1/README.md index 434d063..8cba28e 100644 --- a/TypeScript/17AsyncImporterWithDependency1/README.md +++ b/TypeScript/17AsyncImporterWithDependency1/README.md @@ -1,64 +1,66 @@ +# Welcome to the SPT-AKI Modding Project -This project was created to automate most parts of building and setting up an environment. +This project is designed to streamline the initial setup process for building and creating mods in the SPT-AKI environment. Follow this guide to set up your environment efficiently. -## **NodeJS:** +## **Table of Contents** +- [NodeJS Setup](#nodejs-setup) +- [IDE Setup](#ide-setup) +- [Workspace Configuration](#workspace-configuration) +- [Environment Setup](#environment-setup) +- [Essential Concepts](#essential-concepts) +- [Coding Guidelines](#coding-guidelines) +- [Distribution Guidelines](#distribution-guidelines) -The first step would be to install nodejs on your pc, the version you NEED is **16.17.1** +## **NodeJS Setup** -That version is the one that has been used to test the mod templates and build scripts. +Before you begin, ensure to install NodeJS version `v16.17.1`, which has been tested thoroughly with our mod templates and build scripts. Download it from the [official NodeJS website](https://nodejs.org/). -It can be downloaded from here: https://nodejs.org/dist/v16.17.1/node-v16.17.1-x64.msi +After installation, it's advised to reboot your system. -A system reboot may be needed after install. +## **IDE Setup** -## **IDE:** +For this project, you can work with either [VSCodium](https://vscodium.com/) or [VSCode](https://code.visualstudio.com/). However, we strongly recommend using VSCode, as all development and testing have been carried out using this IDE, ensuring a smoother experience and compatibility with the project setups. Either way, we have a prepared a workspace file to assist you in setting up your environment. -The second step is having an IDE ready. We've setup a VSCodium workspace file to help with this. +## **Workspace Configuration** -You CAN use Visual Studio Code if you so desire, just keep in mind that our devs tests on the mod files was done using VSCode. +With NodeJS and your chosen IDE ready, initiate the `mod.code-workspace` file using your IDE: -You can get VSCode here: https://code.visualstudio.com/ +> File -> Open Workspace from File... -## **Workspace:** +Upon project loading, consider installing recommended plugins like the ESLint plugin. -Once you have NodeJS and VSCode/VSCodium ready, open the mod.code-workspace file with VSCode (File->Open Workspace from File...). +## **Environment Setup** -Once the project loads you may be recommended to install the ESLint plugin. This is HIGHLY recommended. +An automated task is available to configure your environment for Typescript utilization: -## **Environment Setup:** +> Terminal -> Run Task... -> Show All Tasks... -> npm: install -There is a task that will automatically setup your environment to use typescript. +Note: Preserve the `node_modules` folder as it contains necessary dependencies for Typescript and other functionalities. -To run it, you just need to go to: +## **Essential Concepts** -> Terminal->Run Task...->Show All Tasks...->npm: install +Prioritize understanding Dependency Injection and Inversion of Control, the architectural principles SPT-AKI adopts. Comprehensive guidelines will be available on the hub upon release. -After running this task, your environment will be ready to start coding. +Some resources to get you started: + - [A quick intro to Dependency Injection](https://www.freecodecamp.org/news/a-quick-intro-to-dependency-injection-what-it-is-and-when-to-use-it-7578c84fa88f/) + - [Understanding Inversion of Control (IoC) Principle](https://medium.com/@amitkma/understanding-inversion-of-control-ioc-principle-163b1dc97454) -DO NOT remove the node_modules folder, this is an auto generated directory that has the required dependencies to be able to use typescript and more. +## **Coding Guidelines** -## **IMPORTANT:** +Focus your mod development around the `mod.ts` file. In the `package.json` file, only alter these properties: `"name"`, `"version"`, `"license"`, `"author"`, and `"akiVersion"`. -Before starting to work on your mod, we suggest you read about Dependency Injection and Inversion of Control as this is the adopted architecture SPT-AKI has adopted. +New to Typescript? Find comprehensive documentation on the [official website](https://www.typescriptlang.org/docs/). -It will be difficult to understand some of the problems you may be having if you dont understand the basics of it. +## **Distribution Guidelines** -A guide explaining all the essentials will be available on the hub on release for you to read about. +Automated tasks are set up to bundle all necessary files for your mod to function in SPT-AKI: -## **Coding:** +> Terminal -> Run Task... -> Show All Tasks... -> npm: build -All your work should be centered around the mod.ts file as an entry point. -You can ONLY change the following properties from the package.json file: `"name"`, `"version"`, `"license"`: `"MIT"`, `"author"`, `"akiVersion"`. +The ZIP output, located in the `dist` directory, contains all required files. Ensure all files are included and modify the `.buildignore` file as needed. This ZIP file is your uploadable asset for the hub. -If you have never used typescript before, you can read about it here: https://www.typescriptlang.org/docs/ +## **Conclusion** -## **Distributing your mod:** +With this setup, you're ready to begin modding with SPT-AKI. If you run into any trouble be sure to check out the [modding documentation on the hub](https://hub.sp-tarkov.com/doc/lexicon/66-modding/). If you really get stuck feel free to join us in the [#mods-development](https://discord.com/channels/875684761291599922/875803116409323562) official Discord channel. -The project has been set up with an automatic task that will copy and zip ALL required files for your mod to work on SPT-AKI. -To run this task you just need to go to: - -> Terminal->Run Task...->Show All Tasks...->npm: build:zip - -The output will be a mod.zip file that will appear on the root of the project. - -Always verify that all files were included into the zip file. \ No newline at end of file +Build something awesome! diff --git a/TypeScript/17AsyncImporterWithDependency1/build.mjs b/TypeScript/17AsyncImporterWithDependency1/build.mjs new file mode 100644 index 0000000..d00117c --- /dev/null +++ b/TypeScript/17AsyncImporterWithDependency1/build.mjs @@ -0,0 +1,383 @@ +#!/usr/bin/env node + +/** + * Build Script + * + * This script automates the build process for server-side SPT mod projects, facilitating the creation of distributable + * mod packages. It performs a series of operations as outlined below: + * - Loads the .buildignore file, which is used to list files that should be ignored during the build process. + * - Loads the package.json to get project details so a descriptive name can be created for the mod package. + * - Creates a distribution directory and a temporary working directory. + * - Copies files to the temporary directory while respecting the .buildignore rules. + * - Creates a zip archive of the project files. + * - Moves the zip file to the root of the distribution directory. + * - Cleans up the temporary directory. + * + * It's typical that this script be customized to suit the needs of each project. For example, the script can be updated + * to perform additional operations, such as moving the mod package to a specific location or uploading it to a server. + * This script is intended to be a starting point for developers to build upon. + * + * Usage: + * - Run this script using npm: `npm run build` + * - Use `npm run buildinfo` for detailed logging. + * + * Note: + * - Ensure that all necessary Node.js modules are installed before running the script: `npm install` + * - The script reads configurations from the `package.json` and `.buildignore` files; ensure they are correctly set up. + * + * @author Refringe + * @version v1.0.0 + */ + +import fs from "fs-extra"; +import os from "os"; +import path from "path"; +import { fileURLToPath } from "url"; +import { dirname } from "path"; +import ignore from "ignore"; +import archiver from "archiver"; +import winston from "winston"; + +// Get the command line arguments to determine whether to use verbose logging. +const args = process.argv.slice(2); +const verbose = args.includes("--verbose") || args.includes("-v"); + +// Configure the Winston logger to use colours. +const logColors = { + error: "red", + warn: "yellow", + info: "grey", + success: "green", +}; +winston.addColors(logColors); + +// Create a logger instance to log build progress. Configure the logger levels to allow for different levels of logging +// based on the verbosity flag, and set the console transport to log messages of the appropriate level. +const logger = winston.createLogger({ + levels: { + error: 0, + warn: 1, + success: 2, + info: 3, + }, + format: winston.format.combine( + winston.format.colorize(), + winston.format.printf(info => { + return `${info.level}: ${info.message}`; + }) + ), + transports: [ + new winston.transports.Console({ + level: verbose ? "info" : "success", + }), + ], +}); + +/** + * The main function orchestrates the build process for creating a distributable mod package. It leverages a series of + * helper functions to perform various tasks such as loading configuration files, setting up directories, copying files + * according to `.buildignore` rules, and creating a ZIP archive of the project files. + * + * Utilizes the Winston logger to provide information on the build status at different stages of the process. + * + * @returns {void} + */ +async function main() { + // Get the current directory where the script is being executed + const currentDir = getCurrentDirectory(); + + // Defining at this scope because we need to use it in the finally block. + let projectDir; + + try { + // Load the .buildignore file to set up an ignore handler for the build process. + const buildIgnorePatterns = await loadBuildIgnoreFile(currentDir); + + // Load the package.json file to get project details. + const packageJson = await loadPackageJson(currentDir); + + // Create a descriptive name for the mod package. + const projectName = createProjectName(packageJson); + logger.log("success", `Project name created: ${projectName}`); + + // Remove the old distribution directory and create a fresh one. + const distDir = await removeOldDistDirectory(currentDir); + logger.log("info", "Distribution directory successfully cleaned."); + + // Create a temporary working directory to perform the build operations. + projectDir = await createTemporaryDirectoryWithProjectName(projectName); + logger.log("success", "Temporary working directory successfully created."); + logger.log("info", projectDir); + + // Copy files to the temporary directory while respecting the .buildignore rules. + logger.log("info", "Beginning copy operation using .buildignore file..."); + await copyFiles(currentDir, projectDir, buildIgnorePatterns); + logger.log("success", "Files successfully copied to temporary directory."); + + // Create a zip archive of the project files. + logger.log("info", "Beginning folder compression..."); + const zipFilePath = path.join(path.dirname(projectDir), `${projectName}.zip`); + await createZipFile(projectDir, zipFilePath, "user/mods/" + projectName); + logger.log("success", "Archive successfully created."); + logger.log("info", zipFilePath); + + // Move the zip file inside of the project directory, within the temporary working directory. + const zipFileInProjectDir = path.join(projectDir, `${projectName}.zip`); + await fs.move(zipFilePath, zipFileInProjectDir); + logger.log("success", "Archive successfully moved."); + logger.log("info", zipFileInProjectDir); + + // Move the temporary directory into the distribution directory. + await fs.move(projectDir, distDir); + logger.log("success", "Temporary directory successfully moved into project distribution directory."); + + // Log the success message. Write out the path to the mod package. + logger.log("success", "------------------------------------"); + logger.log("success", "Build script completed successfully!"); + logger.log("success", "Your mod package has been created in the 'dist' directory:"); + logger.log("success", `/${path.relative(process.cwd(), path.join(distDir, `${projectName}.zip`))}`); + logger.log("success", "------------------------------------"); + if (!verbose) { + logger.log("success", "To see a detailed build log, use `npm run buildinfo`."); + logger.log("success", "------------------------------------"); + } + } catch (err) { + // If any of the file operations fail, log the error. + logger.log("error", "An error occurred: " + err); + } finally { + // Clean up the temporary directory, even if the build fails. + if (projectDir) { + try { + await fs.promises.rm(projectDir, { force: true, recursive: true }); + logger.log("info", "Cleaned temporary directory."); + } catch (err) { + logger.log("error", "Failed to clean temporary directory: " + err); + } + } + } +} + +/** + * Retrieves the current working directory where the script is being executed. This directory is used as a reference + * point for various file operations throughout the build process, ensuring that paths are resolved correctly regardless + * of the location from which the script is invoked. + * + * @returns {string} The absolute path of the current working directory. + */ +function getCurrentDirectory() { + return dirname(fileURLToPath(import.meta.url)); +} + +/** + * Loads the `.buildignore` file and sets up an ignore handler using the `ignore` module. The `.buildignore` file + * contains a list of patterns describing files and directories that should be ignored during the build process. The + * ignore handler created by this method is used to filter files and directories when copying them to the temporary + * directory, ensuring that only necessary files are included in the final mod package. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to an ignore handler. + */ +async function loadBuildIgnoreFile(currentDir) { + const buildIgnorePath = path.join(currentDir, ".buildignore"); + + try { + // Attempt to read the contents of the .buildignore file asynchronously. + const fileContent = await fs.promises.readFile(buildIgnorePath, "utf-8"); + + // Return a new ignore instance and add the rules from the .buildignore file (split by newlines). + return ignore().add(fileContent.split("\n")); + } catch (err) { + logger.log("warn", "Failed to read .buildignore file. No files or directories will be ignored."); + + // Return an empty ignore instance, ensuring the build process can continue. + return ignore(); + } +} + +/** + * Loads the `package.json` file and returns its content as a JSON object. The `package.json` file contains important + * project details such as the name and version, which are used in later stages of the build process to create a + * descriptive name for the mod package. The method reads the file from the current working directory, ensuring that it + * accurately reflects the current state of the project. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to a JSON object containing the contents of the `package.json`. + */ +async function loadPackageJson(currentDir) { + const packageJsonPath = path.join(currentDir, "package.json"); + + // Read the contents of the package.json file asynchronously as a UTF-8 string. + const packageJsonContent = await fs.promises.readFile(packageJsonPath, "utf-8"); + + return JSON.parse(packageJsonContent); +} + +/** + * Constructs a descriptive name for the mod package using details from the `package.json` file. The name is created by + * concatenating the project name, version, and a timestamp, resulting in a unique and descriptive file name for each + * build. This name is used as the base name for the temporary working directory and the final ZIP archive, helping to + * identify different versions of the mod package easily. + * + * @param {Object} packageJson - A JSON object containing the contents of the `package.json` file. + * @returns {string} A string representing the constructed project name. + */ +function createProjectName(packageJson) { + // Remove any non-alphanumeric characters from the author and name. + const author = packageJson.author.replace(/\W/g, ""); + const name = packageJson.name.replace(/\W/g, ""); + const version = packageJson.version; + + // Ensure the name is lowercase, as per the package.json specification. + return `${author}-${name}-${version}`.toLowerCase(); +} + +/** + * Defines the location of the distribution directory where the final mod package will be stored and deletes any + * existing distribution directory to ensure a clean slate for the build process. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to the absolute path to the distribution directory. + */ +async function removeOldDistDirectory(projectDir) { + const distPath = path.join(projectDir, "dist"); + await fs.remove(distPath); + return distPath; +} + +/** + * Creates a temporary working directory using the project name. This directory serves as a staging area where project + * files are gathered before being archived into the final mod package. The method constructs a unique directory path + * by appending the project name to a base temporary directory path, ensuring that each build has its own isolated + * working space. This approach facilitates clean and organized build processes, avoiding potential conflicts with other + * builds. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @param {string} projectName - The constructed project name, used to create a unique path for the temporary directory. + * @returns {Promise} A promise that resolves to the absolute path of the newly created temporary directory. + */ +async function createTemporaryDirectoryWithProjectName(projectName) { + // Create a new directory in the system's temporary folder to hold the project files. + const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "spt-mod-build-")); + + // Create a subdirectory within the temporary directory using the project name for this specific build. + const projectDir = path.join(tempDir, projectName); + await fs.ensureDir(projectDir); + + return projectDir; +} + +/** + * Copies the project files to the temporary directory while respecting the rules defined in the `.buildignore` file. + * The method is recursive, iterating over all files and directories in the source directory and using the ignore + * handler to filter out files and directories that match the patterns defined in the `.buildignore` file. This ensures + * that only the necessary files are included in the final mod package, adhering to the specifications defined by the + * developer in the `.buildignore` file. + * + * The copy operations are delayed and executed in parallel to improve efficiency and reduce the build time. This is + * achieved by creating an array of copy promises and awaiting them all at the end of the function. + * + * @param {string} sourceDirectory - The absolute path of the current working directory. + * @param {string} destinationDirectory - The absolute path of the temporary directory where the files will be copied. + * @param {Ignore} ignoreHandler - The ignore handler created from the `.buildignore` file. + * @returns {Promise} A promise that resolves when all copy operations are completed successfully. + */ +async function copyFiles(srcDir, destDir, ignoreHandler) { + try { + // Read the contents of the source directory to get a list of entries (files and directories). + const entries = await fs.promises.readdir(srcDir, { withFileTypes: true }); + + // Initialize an array to hold the promises returned by recursive calls to copyFiles and copyFile operations. + const copyOperations = []; + + for (const entry of entries) { + // Define the source and destination paths for each entry. + const srcPath = path.join(srcDir, entry.name); + const destPath = path.join(destDir, entry.name); + + // Get the relative path of the source file to check against the ignore handler. + const relativePath = path.relative(process.cwd(), srcPath); + + // If the ignore handler dictates that this file should be ignored, skip to the next iteration. + if (ignoreHandler.ignores(relativePath)) { + logger.log("info", `Ignored: /${path.relative(process.cwd(), srcPath)}`); + continue; + } + + if (entry.isDirectory()) { + // If the entry is a directory, create the corresponding temporary directory and make a recursive call + // to copyFiles to handle copying the contents of the directory. + await fs.ensureDir(destPath); + copyOperations.push(copyFiles(srcPath, destPath, ignoreHandler)); + } else { + // If the entry is a file, add a copyFile operation to the copyOperations array and log the event when + // the operation is successful. + copyOperations.push( + fs.copy(srcPath, destPath).then(() => { + logger.log("info", `Copied: /${path.relative(process.cwd(), srcPath)}`); + }) + ); + } + } + + // Await all copy operations to ensure all files and directories are copied before exiting the function. + await Promise.all(copyOperations); + } catch (err) { + // Log an error message if any error occurs during the copy process. + logger.log("error", "Error copying files: " + err); + } +} + +/** + * Creates a ZIP archive of the project files located in the temporary directory. The method uses the `archiver` module + * to create a ZIP file, which includes all the files that have been copied to the temporary directory during the build + * process. The ZIP file is named using the project name, helping to identify the contents of the archive easily. + * + * @param {string} directoryPath - The absolute path of the temporary directory containing the project files. + * @param {string} projectName - The constructed project name, used to name the ZIP file. + * @returns {Promise} A promise that resolves to the absolute path of the created ZIP file. + */ +async function createZipFile(directoryToZip, zipFilePath, containerDirName) { + return new Promise((resolve, reject) => { + // Create a write stream to the specified ZIP file path. + const output = fs.createWriteStream(zipFilePath); + + // Create a new archiver instance with ZIP format and maximum compression level. + const archive = archiver("zip", { + zlib: { level: 9 }, + }); + + // Set up an event listener for the 'close' event to resolve the promise when the archiver has finalized. + output.on("close", function () { + logger.log("info", "Archiver has finalized. The output and the file descriptor have closed."); + resolve(); + }); + + // Set up an event listener for the 'warning' event to handle warnings appropriately, logging them or rejecting + // the promise based on the error code. + archive.on("warning", function (err) { + if (err.code === "ENOENT") { + logger.log("warn", `Archiver issued a warning: ${err.code} - ${err.message}`); + } else { + reject(err); + } + }); + + // Set up an event listener for the 'error' event to reject the promise if any error occurs during archiving. + archive.on("error", function (err) { + reject(err); + }); + + // Pipe archive data to the file. + archive.pipe(output); + + // Add the directory to the archive, under the provided directory name. + archive.directory(directoryToZip, containerDirName); + + // Finalize the archive, indicating that no more files will be added and triggering the 'close' event once all + // data has been written. + archive.finalize(); + }); +} + +// Engage! +main(); diff --git a/TypeScript/17AsyncImporterWithDependency1/package.json b/TypeScript/17AsyncImporterWithDependency1/package.json index 8db4c99..62af74f 100644 --- a/TypeScript/17AsyncImporterWithDependency1/package.json +++ b/TypeScript/17AsyncImporterWithDependency1/package.json @@ -10,17 +10,20 @@ }, "scripts": { "setup": "npm i", - "build": "node ./packageBuild.ts" + "build": "node ./build.mjs", + "buildinfo": "node ./build.mjs --verbose" }, "devDependencies": { "@types/node": "20.4.5", "@typescript-eslint/eslint-plugin": "6.2.0", "@typescript-eslint/parser": "6.2.0", - "bestzip": "2.2.1", + "archiver": "^6.0", "eslint": "8.46.0", - "fs-extra": "11.1.1", - "glob": "10.3.3", + "fs-extra": "^11.1", + "ignore": "^5.2", + "os": "^0.1", "tsyringe": "4.8.0", - "typescript": "5.1.6" + "typescript": "5.1.6", + "winston": "^3.9" } -} \ No newline at end of file +} diff --git a/TypeScript/17AsyncImporterWithDependency1/packageBuild.ts b/TypeScript/17AsyncImporterWithDependency1/packageBuild.ts deleted file mode 100644 index d76ddd8..0000000 --- a/TypeScript/17AsyncImporterWithDependency1/packageBuild.ts +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env node - -// This is a simple script used to build a mod package. The script will copy necessary files to the build directory -// and compress the build directory into a zip file that can be easily shared. - -const fs = require("fs-extra"); -const glob = require("glob"); -const zip = require('bestzip'); -const path = require("path"); -const minimatch = require('minimatch'); - -// Load the package.json file to get some information about the package so we can name things appropriately. This is -// atypical, and you would never do this in a production environment, but this script is only used for development so -// it's fine in this case. Some of these values are stored in environment variables, but those differ between node -// versions; the 'author' value is not available after node v14. -const { author, name:packageName, version } = require("./package.json"); - -// Generate the name of the package, stripping out all non-alphanumeric characters in the 'author' and 'name'. -const modName = `${author.replace(/[^a-z0-9]/gi, "")}-${packageName.replace(/[^a-z0-9]/gi, "")}-${version}`; -console.log(`Generated package name: ${modName}`); - -// Delete the old build directory and compressed package file. -fs.rmSync(`${__dirname}/dist`, { force: true, recursive: true }); -console.log("Previous build files deleted."); - -// Generate a list of files that should not be copied over into the distribution directory. This is a blacklist to ensure -// we always copy over additional files and directories that authors may have added to their project. This may need to be -// expanded upon by the mod author to allow for node modules that are used within the mod; example commented out below. -const ignoreList = [ - "node_modules/", - // "node_modules/!(weighted|glob)", // Instead of excluding the entire node_modules directory, allow two node modules. - "src/**/*.js", - "types/", - ".git/", - ".gitea/", - ".eslintignore", - ".eslintrc.json", - ".gitignore", - ".DS_Store", - "packageBuild.ts", - "mod.code-workspace", - "package-lock.json", - "tsconfig.json" -]; -const exclude = glob.sync(`{${ignoreList.join(",")}}`, { realpath: true, dot: true }); - -fs.copySync(__dirname, path.normalize(`${__dirname}/../~${modName}`), {filter: (src) => -{ - const relativePath = path.relative(__dirname, src); - const shouldExclude = exclude.some((pattern) => minimatch(relativePath, pattern)); - console.log(`${relativePath} - Excluded: ${shouldExclude}`); - return !shouldExclude; -},}); - -fs.moveSync(path.normalize(`${__dirname}/../~${modName}`), path.normalize(`${__dirname}/${modName}`), { overwrite: true }); -fs.copySync(path.normalize(`${__dirname}/${modName}`), path.normalize(`${__dirname}/dist`)); -console.log("Build files copied."); - -// Compress the files for easy distribution. The compressed file is saved into the dist directory. When uncompressed we -// need to be sure that it includes a directory that the user can easily copy into their game mods directory. -zip({ - source: modName, - destination: `dist/${modName}.zip`, - cwd: __dirname -}).catch(function(err) -{ - console.error("A bestzip error has occurred: ", err.stack); -}).then(function() -{ - console.log(`Compressed mod package to: /dist/${modName}.zip`); - - // Now that we're done with the compression we can delete the temporary build directory. - fs.rmSync(`${__dirname}/${modName}`, { force: true, recursive: true }); - console.log("Build successful! your zip file has been created and is ready to be uploaded to hub.sp-tarkov.com/files/"); -}); \ No newline at end of file diff --git a/TypeScript/17AsyncImporterWithDependency2/.buildignore b/TypeScript/17AsyncImporterWithDependency2/.buildignore new file mode 100644 index 0000000..2cbde65 --- /dev/null +++ b/TypeScript/17AsyncImporterWithDependency2/.buildignore @@ -0,0 +1,20 @@ +/.buildignore +/.DS_Store +/.editorconfig +/.eslintignore +/.eslintrc.json +/.git +/.github +/.gitignore +/.gitlab +/.nvmrc +/.prettierrc +/.vscode +/build.mjs +/dist +/images +/mod.code-workspace +/node_modules +/package-lock.json +/tsconfig.json +/types diff --git a/TypeScript/17AsyncImporterWithDependency2/.eslintrc.json b/TypeScript/17AsyncImporterWithDependency2/.eslintrc.json index 98e7a0c..071a313 100644 --- a/TypeScript/17AsyncImporterWithDependency2/.eslintrc.json +++ b/TypeScript/17AsyncImporterWithDependency2/.eslintrc.json @@ -1,81 +1,98 @@ { - "root": true, - "parser": "@typescript-eslint/parser", - "plugins": [ - "@typescript-eslint" - ], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended" - ], - "rules": { - "max-len": [ - "warn", - { - "code":100 - } + "root": true, + "parser": "@typescript-eslint/parser", + "plugins": [ + "@typescript-eslint" ], - "@typescript-eslint/no-explicit-any": 0, - "@typescript-eslint/no-unused-vars": 1, - "@typescript-eslint/no-empty-interface": 0, - "@typescript-eslint/no-namespace": 0, - "@typescript-eslint/comma-dangle": 1, - "@typescript-eslint/func-call-spacing": 2, - "@typescript-eslint/quotes": 1, - "@typescript-eslint/brace-style": [ - "warn", - "allman" + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" ], - "@typescript-eslint/naming-convention": [ - "warn", - { - "selector": "default", - "format": ["camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "typeLike", - "format": ["PascalCase"] - }, - { - "selector": "objectLiteralProperty", - "format": ["PascalCase", "camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "typeProperty", - "format": ["PascalCase", "camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "enumMember", - "format": ["UPPER_CASE"] - } - ], - "@typescript-eslint/indent": [ - "warn", - 4 - ], - "@typescript-eslint/no-unused-expressions": [ - "warn", - { - "allowShortCircuit": false, - "allowTernary": false - } - ], - "@typescript-eslint/keyword-spacing": [ - "warn", - { - "before": true, - "after": true - } - ], - "@typescript-eslint/explicit-module-boundary-types": [ - "warn", - { - "allowArgumentsExplicitlyTypedAsAny": true - } + "rules": { + "@typescript-eslint/no-explicit-any": 0, + "@typescript-eslint/no-unused-vars": 1, + "@typescript-eslint/no-empty-interface": 0, + "@typescript-eslint/no-namespace": 0, + "@typescript-eslint/comma-dangle": 1, + "@typescript-eslint/func-call-spacing": 2, + "@typescript-eslint/quotes": 1, + "@typescript-eslint/brace-style": [ + "warn", + "allman" + ], + "@typescript-eslint/naming-convention": [ + "warn", + { + "selector": "default", + "format": [ + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "typeLike", + "format": [ + "PascalCase" + ] + }, + { + "selector": "objectLiteralProperty", + "format": [ + "PascalCase", + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "typeProperty", + "format": [ + "PascalCase", + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "enumMember", + "format": [ + "UPPER_CASE" + ] + } + ], + "@typescript-eslint/indent": [ + "warn", + 4 + ], + "@typescript-eslint/no-unused-expressions": [ + "warn", + { + "allowShortCircuit": false, + "allowTernary": false + } + ], + "@typescript-eslint/keyword-spacing": [ + "warn", + { + "before": true, + "after": true + } + ], + "@typescript-eslint/explicit-module-boundary-types": [ + "warn", + { + "allowArgumentsExplicitlyTypedAsAny": true + } + ] + }, + "overrides": [ + { + "files": [ + "*.mjs", + "*.ts" + ], + "env": { + "node": true + } + } ] - } } \ No newline at end of file diff --git a/TypeScript/17AsyncImporterWithDependency2/README.md b/TypeScript/17AsyncImporterWithDependency2/README.md index 434d063..8cba28e 100644 --- a/TypeScript/17AsyncImporterWithDependency2/README.md +++ b/TypeScript/17AsyncImporterWithDependency2/README.md @@ -1,64 +1,66 @@ +# Welcome to the SPT-AKI Modding Project -This project was created to automate most parts of building and setting up an environment. +This project is designed to streamline the initial setup process for building and creating mods in the SPT-AKI environment. Follow this guide to set up your environment efficiently. -## **NodeJS:** +## **Table of Contents** +- [NodeJS Setup](#nodejs-setup) +- [IDE Setup](#ide-setup) +- [Workspace Configuration](#workspace-configuration) +- [Environment Setup](#environment-setup) +- [Essential Concepts](#essential-concepts) +- [Coding Guidelines](#coding-guidelines) +- [Distribution Guidelines](#distribution-guidelines) -The first step would be to install nodejs on your pc, the version you NEED is **16.17.1** +## **NodeJS Setup** -That version is the one that has been used to test the mod templates and build scripts. +Before you begin, ensure to install NodeJS version `v16.17.1`, which has been tested thoroughly with our mod templates and build scripts. Download it from the [official NodeJS website](https://nodejs.org/). -It can be downloaded from here: https://nodejs.org/dist/v16.17.1/node-v16.17.1-x64.msi +After installation, it's advised to reboot your system. -A system reboot may be needed after install. +## **IDE Setup** -## **IDE:** +For this project, you can work with either [VSCodium](https://vscodium.com/) or [VSCode](https://code.visualstudio.com/). However, we strongly recommend using VSCode, as all development and testing have been carried out using this IDE, ensuring a smoother experience and compatibility with the project setups. Either way, we have a prepared a workspace file to assist you in setting up your environment. -The second step is having an IDE ready. We've setup a VSCodium workspace file to help with this. +## **Workspace Configuration** -You CAN use Visual Studio Code if you so desire, just keep in mind that our devs tests on the mod files was done using VSCode. +With NodeJS and your chosen IDE ready, initiate the `mod.code-workspace` file using your IDE: -You can get VSCode here: https://code.visualstudio.com/ +> File -> Open Workspace from File... -## **Workspace:** +Upon project loading, consider installing recommended plugins like the ESLint plugin. -Once you have NodeJS and VSCode/VSCodium ready, open the mod.code-workspace file with VSCode (File->Open Workspace from File...). +## **Environment Setup** -Once the project loads you may be recommended to install the ESLint plugin. This is HIGHLY recommended. +An automated task is available to configure your environment for Typescript utilization: -## **Environment Setup:** +> Terminal -> Run Task... -> Show All Tasks... -> npm: install -There is a task that will automatically setup your environment to use typescript. +Note: Preserve the `node_modules` folder as it contains necessary dependencies for Typescript and other functionalities. -To run it, you just need to go to: +## **Essential Concepts** -> Terminal->Run Task...->Show All Tasks...->npm: install +Prioritize understanding Dependency Injection and Inversion of Control, the architectural principles SPT-AKI adopts. Comprehensive guidelines will be available on the hub upon release. -After running this task, your environment will be ready to start coding. +Some resources to get you started: + - [A quick intro to Dependency Injection](https://www.freecodecamp.org/news/a-quick-intro-to-dependency-injection-what-it-is-and-when-to-use-it-7578c84fa88f/) + - [Understanding Inversion of Control (IoC) Principle](https://medium.com/@amitkma/understanding-inversion-of-control-ioc-principle-163b1dc97454) -DO NOT remove the node_modules folder, this is an auto generated directory that has the required dependencies to be able to use typescript and more. +## **Coding Guidelines** -## **IMPORTANT:** +Focus your mod development around the `mod.ts` file. In the `package.json` file, only alter these properties: `"name"`, `"version"`, `"license"`, `"author"`, and `"akiVersion"`. -Before starting to work on your mod, we suggest you read about Dependency Injection and Inversion of Control as this is the adopted architecture SPT-AKI has adopted. +New to Typescript? Find comprehensive documentation on the [official website](https://www.typescriptlang.org/docs/). -It will be difficult to understand some of the problems you may be having if you dont understand the basics of it. +## **Distribution Guidelines** -A guide explaining all the essentials will be available on the hub on release for you to read about. +Automated tasks are set up to bundle all necessary files for your mod to function in SPT-AKI: -## **Coding:** +> Terminal -> Run Task... -> Show All Tasks... -> npm: build -All your work should be centered around the mod.ts file as an entry point. -You can ONLY change the following properties from the package.json file: `"name"`, `"version"`, `"license"`: `"MIT"`, `"author"`, `"akiVersion"`. +The ZIP output, located in the `dist` directory, contains all required files. Ensure all files are included and modify the `.buildignore` file as needed. This ZIP file is your uploadable asset for the hub. -If you have never used typescript before, you can read about it here: https://www.typescriptlang.org/docs/ +## **Conclusion** -## **Distributing your mod:** +With this setup, you're ready to begin modding with SPT-AKI. If you run into any trouble be sure to check out the [modding documentation on the hub](https://hub.sp-tarkov.com/doc/lexicon/66-modding/). If you really get stuck feel free to join us in the [#mods-development](https://discord.com/channels/875684761291599922/875803116409323562) official Discord channel. -The project has been set up with an automatic task that will copy and zip ALL required files for your mod to work on SPT-AKI. -To run this task you just need to go to: - -> Terminal->Run Task...->Show All Tasks...->npm: build:zip - -The output will be a mod.zip file that will appear on the root of the project. - -Always verify that all files were included into the zip file. \ No newline at end of file +Build something awesome! diff --git a/TypeScript/17AsyncImporterWithDependency2/build.mjs b/TypeScript/17AsyncImporterWithDependency2/build.mjs new file mode 100644 index 0000000..d00117c --- /dev/null +++ b/TypeScript/17AsyncImporterWithDependency2/build.mjs @@ -0,0 +1,383 @@ +#!/usr/bin/env node + +/** + * Build Script + * + * This script automates the build process for server-side SPT mod projects, facilitating the creation of distributable + * mod packages. It performs a series of operations as outlined below: + * - Loads the .buildignore file, which is used to list files that should be ignored during the build process. + * - Loads the package.json to get project details so a descriptive name can be created for the mod package. + * - Creates a distribution directory and a temporary working directory. + * - Copies files to the temporary directory while respecting the .buildignore rules. + * - Creates a zip archive of the project files. + * - Moves the zip file to the root of the distribution directory. + * - Cleans up the temporary directory. + * + * It's typical that this script be customized to suit the needs of each project. For example, the script can be updated + * to perform additional operations, such as moving the mod package to a specific location or uploading it to a server. + * This script is intended to be a starting point for developers to build upon. + * + * Usage: + * - Run this script using npm: `npm run build` + * - Use `npm run buildinfo` for detailed logging. + * + * Note: + * - Ensure that all necessary Node.js modules are installed before running the script: `npm install` + * - The script reads configurations from the `package.json` and `.buildignore` files; ensure they are correctly set up. + * + * @author Refringe + * @version v1.0.0 + */ + +import fs from "fs-extra"; +import os from "os"; +import path from "path"; +import { fileURLToPath } from "url"; +import { dirname } from "path"; +import ignore from "ignore"; +import archiver from "archiver"; +import winston from "winston"; + +// Get the command line arguments to determine whether to use verbose logging. +const args = process.argv.slice(2); +const verbose = args.includes("--verbose") || args.includes("-v"); + +// Configure the Winston logger to use colours. +const logColors = { + error: "red", + warn: "yellow", + info: "grey", + success: "green", +}; +winston.addColors(logColors); + +// Create a logger instance to log build progress. Configure the logger levels to allow for different levels of logging +// based on the verbosity flag, and set the console transport to log messages of the appropriate level. +const logger = winston.createLogger({ + levels: { + error: 0, + warn: 1, + success: 2, + info: 3, + }, + format: winston.format.combine( + winston.format.colorize(), + winston.format.printf(info => { + return `${info.level}: ${info.message}`; + }) + ), + transports: [ + new winston.transports.Console({ + level: verbose ? "info" : "success", + }), + ], +}); + +/** + * The main function orchestrates the build process for creating a distributable mod package. It leverages a series of + * helper functions to perform various tasks such as loading configuration files, setting up directories, copying files + * according to `.buildignore` rules, and creating a ZIP archive of the project files. + * + * Utilizes the Winston logger to provide information on the build status at different stages of the process. + * + * @returns {void} + */ +async function main() { + // Get the current directory where the script is being executed + const currentDir = getCurrentDirectory(); + + // Defining at this scope because we need to use it in the finally block. + let projectDir; + + try { + // Load the .buildignore file to set up an ignore handler for the build process. + const buildIgnorePatterns = await loadBuildIgnoreFile(currentDir); + + // Load the package.json file to get project details. + const packageJson = await loadPackageJson(currentDir); + + // Create a descriptive name for the mod package. + const projectName = createProjectName(packageJson); + logger.log("success", `Project name created: ${projectName}`); + + // Remove the old distribution directory and create a fresh one. + const distDir = await removeOldDistDirectory(currentDir); + logger.log("info", "Distribution directory successfully cleaned."); + + // Create a temporary working directory to perform the build operations. + projectDir = await createTemporaryDirectoryWithProjectName(projectName); + logger.log("success", "Temporary working directory successfully created."); + logger.log("info", projectDir); + + // Copy files to the temporary directory while respecting the .buildignore rules. + logger.log("info", "Beginning copy operation using .buildignore file..."); + await copyFiles(currentDir, projectDir, buildIgnorePatterns); + logger.log("success", "Files successfully copied to temporary directory."); + + // Create a zip archive of the project files. + logger.log("info", "Beginning folder compression..."); + const zipFilePath = path.join(path.dirname(projectDir), `${projectName}.zip`); + await createZipFile(projectDir, zipFilePath, "user/mods/" + projectName); + logger.log("success", "Archive successfully created."); + logger.log("info", zipFilePath); + + // Move the zip file inside of the project directory, within the temporary working directory. + const zipFileInProjectDir = path.join(projectDir, `${projectName}.zip`); + await fs.move(zipFilePath, zipFileInProjectDir); + logger.log("success", "Archive successfully moved."); + logger.log("info", zipFileInProjectDir); + + // Move the temporary directory into the distribution directory. + await fs.move(projectDir, distDir); + logger.log("success", "Temporary directory successfully moved into project distribution directory."); + + // Log the success message. Write out the path to the mod package. + logger.log("success", "------------------------------------"); + logger.log("success", "Build script completed successfully!"); + logger.log("success", "Your mod package has been created in the 'dist' directory:"); + logger.log("success", `/${path.relative(process.cwd(), path.join(distDir, `${projectName}.zip`))}`); + logger.log("success", "------------------------------------"); + if (!verbose) { + logger.log("success", "To see a detailed build log, use `npm run buildinfo`."); + logger.log("success", "------------------------------------"); + } + } catch (err) { + // If any of the file operations fail, log the error. + logger.log("error", "An error occurred: " + err); + } finally { + // Clean up the temporary directory, even if the build fails. + if (projectDir) { + try { + await fs.promises.rm(projectDir, { force: true, recursive: true }); + logger.log("info", "Cleaned temporary directory."); + } catch (err) { + logger.log("error", "Failed to clean temporary directory: " + err); + } + } + } +} + +/** + * Retrieves the current working directory where the script is being executed. This directory is used as a reference + * point for various file operations throughout the build process, ensuring that paths are resolved correctly regardless + * of the location from which the script is invoked. + * + * @returns {string} The absolute path of the current working directory. + */ +function getCurrentDirectory() { + return dirname(fileURLToPath(import.meta.url)); +} + +/** + * Loads the `.buildignore` file and sets up an ignore handler using the `ignore` module. The `.buildignore` file + * contains a list of patterns describing files and directories that should be ignored during the build process. The + * ignore handler created by this method is used to filter files and directories when copying them to the temporary + * directory, ensuring that only necessary files are included in the final mod package. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to an ignore handler. + */ +async function loadBuildIgnoreFile(currentDir) { + const buildIgnorePath = path.join(currentDir, ".buildignore"); + + try { + // Attempt to read the contents of the .buildignore file asynchronously. + const fileContent = await fs.promises.readFile(buildIgnorePath, "utf-8"); + + // Return a new ignore instance and add the rules from the .buildignore file (split by newlines). + return ignore().add(fileContent.split("\n")); + } catch (err) { + logger.log("warn", "Failed to read .buildignore file. No files or directories will be ignored."); + + // Return an empty ignore instance, ensuring the build process can continue. + return ignore(); + } +} + +/** + * Loads the `package.json` file and returns its content as a JSON object. The `package.json` file contains important + * project details such as the name and version, which are used in later stages of the build process to create a + * descriptive name for the mod package. The method reads the file from the current working directory, ensuring that it + * accurately reflects the current state of the project. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to a JSON object containing the contents of the `package.json`. + */ +async function loadPackageJson(currentDir) { + const packageJsonPath = path.join(currentDir, "package.json"); + + // Read the contents of the package.json file asynchronously as a UTF-8 string. + const packageJsonContent = await fs.promises.readFile(packageJsonPath, "utf-8"); + + return JSON.parse(packageJsonContent); +} + +/** + * Constructs a descriptive name for the mod package using details from the `package.json` file. The name is created by + * concatenating the project name, version, and a timestamp, resulting in a unique and descriptive file name for each + * build. This name is used as the base name for the temporary working directory and the final ZIP archive, helping to + * identify different versions of the mod package easily. + * + * @param {Object} packageJson - A JSON object containing the contents of the `package.json` file. + * @returns {string} A string representing the constructed project name. + */ +function createProjectName(packageJson) { + // Remove any non-alphanumeric characters from the author and name. + const author = packageJson.author.replace(/\W/g, ""); + const name = packageJson.name.replace(/\W/g, ""); + const version = packageJson.version; + + // Ensure the name is lowercase, as per the package.json specification. + return `${author}-${name}-${version}`.toLowerCase(); +} + +/** + * Defines the location of the distribution directory where the final mod package will be stored and deletes any + * existing distribution directory to ensure a clean slate for the build process. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to the absolute path to the distribution directory. + */ +async function removeOldDistDirectory(projectDir) { + const distPath = path.join(projectDir, "dist"); + await fs.remove(distPath); + return distPath; +} + +/** + * Creates a temporary working directory using the project name. This directory serves as a staging area where project + * files are gathered before being archived into the final mod package. The method constructs a unique directory path + * by appending the project name to a base temporary directory path, ensuring that each build has its own isolated + * working space. This approach facilitates clean and organized build processes, avoiding potential conflicts with other + * builds. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @param {string} projectName - The constructed project name, used to create a unique path for the temporary directory. + * @returns {Promise} A promise that resolves to the absolute path of the newly created temporary directory. + */ +async function createTemporaryDirectoryWithProjectName(projectName) { + // Create a new directory in the system's temporary folder to hold the project files. + const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "spt-mod-build-")); + + // Create a subdirectory within the temporary directory using the project name for this specific build. + const projectDir = path.join(tempDir, projectName); + await fs.ensureDir(projectDir); + + return projectDir; +} + +/** + * Copies the project files to the temporary directory while respecting the rules defined in the `.buildignore` file. + * The method is recursive, iterating over all files and directories in the source directory and using the ignore + * handler to filter out files and directories that match the patterns defined in the `.buildignore` file. This ensures + * that only the necessary files are included in the final mod package, adhering to the specifications defined by the + * developer in the `.buildignore` file. + * + * The copy operations are delayed and executed in parallel to improve efficiency and reduce the build time. This is + * achieved by creating an array of copy promises and awaiting them all at the end of the function. + * + * @param {string} sourceDirectory - The absolute path of the current working directory. + * @param {string} destinationDirectory - The absolute path of the temporary directory where the files will be copied. + * @param {Ignore} ignoreHandler - The ignore handler created from the `.buildignore` file. + * @returns {Promise} A promise that resolves when all copy operations are completed successfully. + */ +async function copyFiles(srcDir, destDir, ignoreHandler) { + try { + // Read the contents of the source directory to get a list of entries (files and directories). + const entries = await fs.promises.readdir(srcDir, { withFileTypes: true }); + + // Initialize an array to hold the promises returned by recursive calls to copyFiles and copyFile operations. + const copyOperations = []; + + for (const entry of entries) { + // Define the source and destination paths for each entry. + const srcPath = path.join(srcDir, entry.name); + const destPath = path.join(destDir, entry.name); + + // Get the relative path of the source file to check against the ignore handler. + const relativePath = path.relative(process.cwd(), srcPath); + + // If the ignore handler dictates that this file should be ignored, skip to the next iteration. + if (ignoreHandler.ignores(relativePath)) { + logger.log("info", `Ignored: /${path.relative(process.cwd(), srcPath)}`); + continue; + } + + if (entry.isDirectory()) { + // If the entry is a directory, create the corresponding temporary directory and make a recursive call + // to copyFiles to handle copying the contents of the directory. + await fs.ensureDir(destPath); + copyOperations.push(copyFiles(srcPath, destPath, ignoreHandler)); + } else { + // If the entry is a file, add a copyFile operation to the copyOperations array and log the event when + // the operation is successful. + copyOperations.push( + fs.copy(srcPath, destPath).then(() => { + logger.log("info", `Copied: /${path.relative(process.cwd(), srcPath)}`); + }) + ); + } + } + + // Await all copy operations to ensure all files and directories are copied before exiting the function. + await Promise.all(copyOperations); + } catch (err) { + // Log an error message if any error occurs during the copy process. + logger.log("error", "Error copying files: " + err); + } +} + +/** + * Creates a ZIP archive of the project files located in the temporary directory. The method uses the `archiver` module + * to create a ZIP file, which includes all the files that have been copied to the temporary directory during the build + * process. The ZIP file is named using the project name, helping to identify the contents of the archive easily. + * + * @param {string} directoryPath - The absolute path of the temporary directory containing the project files. + * @param {string} projectName - The constructed project name, used to name the ZIP file. + * @returns {Promise} A promise that resolves to the absolute path of the created ZIP file. + */ +async function createZipFile(directoryToZip, zipFilePath, containerDirName) { + return new Promise((resolve, reject) => { + // Create a write stream to the specified ZIP file path. + const output = fs.createWriteStream(zipFilePath); + + // Create a new archiver instance with ZIP format and maximum compression level. + const archive = archiver("zip", { + zlib: { level: 9 }, + }); + + // Set up an event listener for the 'close' event to resolve the promise when the archiver has finalized. + output.on("close", function () { + logger.log("info", "Archiver has finalized. The output and the file descriptor have closed."); + resolve(); + }); + + // Set up an event listener for the 'warning' event to handle warnings appropriately, logging them or rejecting + // the promise based on the error code. + archive.on("warning", function (err) { + if (err.code === "ENOENT") { + logger.log("warn", `Archiver issued a warning: ${err.code} - ${err.message}`); + } else { + reject(err); + } + }); + + // Set up an event listener for the 'error' event to reject the promise if any error occurs during archiving. + archive.on("error", function (err) { + reject(err); + }); + + // Pipe archive data to the file. + archive.pipe(output); + + // Add the directory to the archive, under the provided directory name. + archive.directory(directoryToZip, containerDirName); + + // Finalize the archive, indicating that no more files will be added and triggering the 'close' event once all + // data has been written. + archive.finalize(); + }); +} + +// Engage! +main(); diff --git a/TypeScript/17AsyncImporterWithDependency2/package.json b/TypeScript/17AsyncImporterWithDependency2/package.json index 873272d..3ac10f2 100644 --- a/TypeScript/17AsyncImporterWithDependency2/package.json +++ b/TypeScript/17AsyncImporterWithDependency2/package.json @@ -7,17 +7,20 @@ "akiVersion": "~3.7", "scripts": { "setup": "npm i", - "build": "node ./packageBuild.ts" + "build": "node ./build.mjs", + "buildinfo": "node ./build.mjs --verbose" }, "devDependencies": { "@types/node": "20.4.5", "@typescript-eslint/eslint-plugin": "6.2.0", "@typescript-eslint/parser": "6.2.0", - "bestzip": "2.2.1", + "archiver": "^6.0", "eslint": "8.46.0", - "fs-extra": "11.1.1", - "glob": "10.3.3", + "fs-extra": "^11.1", + "ignore": "^5.2", + "os": "^0.1", "tsyringe": "4.8.0", - "typescript": "5.1.6" + "typescript": "5.1.6", + "winston": "^3.9" } -} \ No newline at end of file +} diff --git a/TypeScript/17AsyncImporterWithDependency2/packageBuild.ts b/TypeScript/17AsyncImporterWithDependency2/packageBuild.ts deleted file mode 100644 index d76ddd8..0000000 --- a/TypeScript/17AsyncImporterWithDependency2/packageBuild.ts +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env node - -// This is a simple script used to build a mod package. The script will copy necessary files to the build directory -// and compress the build directory into a zip file that can be easily shared. - -const fs = require("fs-extra"); -const glob = require("glob"); -const zip = require('bestzip'); -const path = require("path"); -const minimatch = require('minimatch'); - -// Load the package.json file to get some information about the package so we can name things appropriately. This is -// atypical, and you would never do this in a production environment, but this script is only used for development so -// it's fine in this case. Some of these values are stored in environment variables, but those differ between node -// versions; the 'author' value is not available after node v14. -const { author, name:packageName, version } = require("./package.json"); - -// Generate the name of the package, stripping out all non-alphanumeric characters in the 'author' and 'name'. -const modName = `${author.replace(/[^a-z0-9]/gi, "")}-${packageName.replace(/[^a-z0-9]/gi, "")}-${version}`; -console.log(`Generated package name: ${modName}`); - -// Delete the old build directory and compressed package file. -fs.rmSync(`${__dirname}/dist`, { force: true, recursive: true }); -console.log("Previous build files deleted."); - -// Generate a list of files that should not be copied over into the distribution directory. This is a blacklist to ensure -// we always copy over additional files and directories that authors may have added to their project. This may need to be -// expanded upon by the mod author to allow for node modules that are used within the mod; example commented out below. -const ignoreList = [ - "node_modules/", - // "node_modules/!(weighted|glob)", // Instead of excluding the entire node_modules directory, allow two node modules. - "src/**/*.js", - "types/", - ".git/", - ".gitea/", - ".eslintignore", - ".eslintrc.json", - ".gitignore", - ".DS_Store", - "packageBuild.ts", - "mod.code-workspace", - "package-lock.json", - "tsconfig.json" -]; -const exclude = glob.sync(`{${ignoreList.join(",")}}`, { realpath: true, dot: true }); - -fs.copySync(__dirname, path.normalize(`${__dirname}/../~${modName}`), {filter: (src) => -{ - const relativePath = path.relative(__dirname, src); - const shouldExclude = exclude.some((pattern) => minimatch(relativePath, pattern)); - console.log(`${relativePath} - Excluded: ${shouldExclude}`); - return !shouldExclude; -},}); - -fs.moveSync(path.normalize(`${__dirname}/../~${modName}`), path.normalize(`${__dirname}/${modName}`), { overwrite: true }); -fs.copySync(path.normalize(`${__dirname}/${modName}`), path.normalize(`${__dirname}/dist`)); -console.log("Build files copied."); - -// Compress the files for easy distribution. The compressed file is saved into the dist directory. When uncompressed we -// need to be sure that it includes a directory that the user can easily copy into their game mods directory. -zip({ - source: modName, - destination: `dist/${modName}.zip`, - cwd: __dirname -}).catch(function(err) -{ - console.error("A bestzip error has occurred: ", err.stack); -}).then(function() -{ - console.log(`Compressed mod package to: /dist/${modName}.zip`); - - // Now that we're done with the compression we can delete the temporary build directory. - fs.rmSync(`${__dirname}/${modName}`, { force: true, recursive: true }); - console.log("Build successful! your zip file has been created and is ready to be uploaded to hub.sp-tarkov.com/files/"); -}); \ No newline at end of file diff --git a/TypeScript/18CustomItemService/.buildignore b/TypeScript/18CustomItemService/.buildignore new file mode 100644 index 0000000..2cbde65 --- /dev/null +++ b/TypeScript/18CustomItemService/.buildignore @@ -0,0 +1,20 @@ +/.buildignore +/.DS_Store +/.editorconfig +/.eslintignore +/.eslintrc.json +/.git +/.github +/.gitignore +/.gitlab +/.nvmrc +/.prettierrc +/.vscode +/build.mjs +/dist +/images +/mod.code-workspace +/node_modules +/package-lock.json +/tsconfig.json +/types diff --git a/TypeScript/18CustomItemService/.eslintrc.json b/TypeScript/18CustomItemService/.eslintrc.json index c505160..071a313 100644 --- a/TypeScript/18CustomItemService/.eslintrc.json +++ b/TypeScript/18CustomItemService/.eslintrc.json @@ -1,75 +1,98 @@ { - "root": true, - "parser": "@typescript-eslint/parser", - "plugins": [ - "@typescript-eslint" - ], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended" - ], - "rules": { - "@typescript-eslint/no-explicit-any": 0, - "@typescript-eslint/no-unused-vars": 1, - "@typescript-eslint/no-empty-interface": 0, - "@typescript-eslint/no-namespace": 0, - "@typescript-eslint/comma-dangle": 1, - "@typescript-eslint/func-call-spacing": 2, - "@typescript-eslint/quotes": 1, - "@typescript-eslint/brace-style": [ - "warn", - "allman" + "root": true, + "parser": "@typescript-eslint/parser", + "plugins": [ + "@typescript-eslint" ], - "@typescript-eslint/naming-convention": [ - "warn", - { - "selector": "default", - "format": ["camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "typeLike", - "format": ["PascalCase"] - }, - { - "selector": "objectLiteralProperty", - "format": ["PascalCase", "camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "typeProperty", - "format": ["PascalCase", "camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "enumMember", - "format": ["UPPER_CASE"] - } + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" ], - "@typescript-eslint/indent": [ - "warn", - 4 - ], - "@typescript-eslint/no-unused-expressions": [ - "warn", - { - "allowShortCircuit": false, - "allowTernary": false - } - ], - "@typescript-eslint/keyword-spacing": [ - "warn", - { - "before": true, - "after": true - } - ], - "@typescript-eslint/explicit-module-boundary-types": [ - "warn", - { - "allowArgumentsExplicitlyTypedAsAny": true - } + "rules": { + "@typescript-eslint/no-explicit-any": 0, + "@typescript-eslint/no-unused-vars": 1, + "@typescript-eslint/no-empty-interface": 0, + "@typescript-eslint/no-namespace": 0, + "@typescript-eslint/comma-dangle": 1, + "@typescript-eslint/func-call-spacing": 2, + "@typescript-eslint/quotes": 1, + "@typescript-eslint/brace-style": [ + "warn", + "allman" + ], + "@typescript-eslint/naming-convention": [ + "warn", + { + "selector": "default", + "format": [ + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "typeLike", + "format": [ + "PascalCase" + ] + }, + { + "selector": "objectLiteralProperty", + "format": [ + "PascalCase", + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "typeProperty", + "format": [ + "PascalCase", + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "enumMember", + "format": [ + "UPPER_CASE" + ] + } + ], + "@typescript-eslint/indent": [ + "warn", + 4 + ], + "@typescript-eslint/no-unused-expressions": [ + "warn", + { + "allowShortCircuit": false, + "allowTernary": false + } + ], + "@typescript-eslint/keyword-spacing": [ + "warn", + { + "before": true, + "after": true + } + ], + "@typescript-eslint/explicit-module-boundary-types": [ + "warn", + { + "allowArgumentsExplicitlyTypedAsAny": true + } + ] + }, + "overrides": [ + { + "files": [ + "*.mjs", + "*.ts" + ], + "env": { + "node": true + } + } ] - } } \ No newline at end of file diff --git a/TypeScript/18CustomItemService/README.md b/TypeScript/18CustomItemService/README.md index 434d063..8cba28e 100644 --- a/TypeScript/18CustomItemService/README.md +++ b/TypeScript/18CustomItemService/README.md @@ -1,64 +1,66 @@ +# Welcome to the SPT-AKI Modding Project -This project was created to automate most parts of building and setting up an environment. +This project is designed to streamline the initial setup process for building and creating mods in the SPT-AKI environment. Follow this guide to set up your environment efficiently. -## **NodeJS:** +## **Table of Contents** +- [NodeJS Setup](#nodejs-setup) +- [IDE Setup](#ide-setup) +- [Workspace Configuration](#workspace-configuration) +- [Environment Setup](#environment-setup) +- [Essential Concepts](#essential-concepts) +- [Coding Guidelines](#coding-guidelines) +- [Distribution Guidelines](#distribution-guidelines) -The first step would be to install nodejs on your pc, the version you NEED is **16.17.1** +## **NodeJS Setup** -That version is the one that has been used to test the mod templates and build scripts. +Before you begin, ensure to install NodeJS version `v16.17.1`, which has been tested thoroughly with our mod templates and build scripts. Download it from the [official NodeJS website](https://nodejs.org/). -It can be downloaded from here: https://nodejs.org/dist/v16.17.1/node-v16.17.1-x64.msi +After installation, it's advised to reboot your system. -A system reboot may be needed after install. +## **IDE Setup** -## **IDE:** +For this project, you can work with either [VSCodium](https://vscodium.com/) or [VSCode](https://code.visualstudio.com/). However, we strongly recommend using VSCode, as all development and testing have been carried out using this IDE, ensuring a smoother experience and compatibility with the project setups. Either way, we have a prepared a workspace file to assist you in setting up your environment. -The second step is having an IDE ready. We've setup a VSCodium workspace file to help with this. +## **Workspace Configuration** -You CAN use Visual Studio Code if you so desire, just keep in mind that our devs tests on the mod files was done using VSCode. +With NodeJS and your chosen IDE ready, initiate the `mod.code-workspace` file using your IDE: -You can get VSCode here: https://code.visualstudio.com/ +> File -> Open Workspace from File... -## **Workspace:** +Upon project loading, consider installing recommended plugins like the ESLint plugin. -Once you have NodeJS and VSCode/VSCodium ready, open the mod.code-workspace file with VSCode (File->Open Workspace from File...). +## **Environment Setup** -Once the project loads you may be recommended to install the ESLint plugin. This is HIGHLY recommended. +An automated task is available to configure your environment for Typescript utilization: -## **Environment Setup:** +> Terminal -> Run Task... -> Show All Tasks... -> npm: install -There is a task that will automatically setup your environment to use typescript. +Note: Preserve the `node_modules` folder as it contains necessary dependencies for Typescript and other functionalities. -To run it, you just need to go to: +## **Essential Concepts** -> Terminal->Run Task...->Show All Tasks...->npm: install +Prioritize understanding Dependency Injection and Inversion of Control, the architectural principles SPT-AKI adopts. Comprehensive guidelines will be available on the hub upon release. -After running this task, your environment will be ready to start coding. +Some resources to get you started: + - [A quick intro to Dependency Injection](https://www.freecodecamp.org/news/a-quick-intro-to-dependency-injection-what-it-is-and-when-to-use-it-7578c84fa88f/) + - [Understanding Inversion of Control (IoC) Principle](https://medium.com/@amitkma/understanding-inversion-of-control-ioc-principle-163b1dc97454) -DO NOT remove the node_modules folder, this is an auto generated directory that has the required dependencies to be able to use typescript and more. +## **Coding Guidelines** -## **IMPORTANT:** +Focus your mod development around the `mod.ts` file. In the `package.json` file, only alter these properties: `"name"`, `"version"`, `"license"`, `"author"`, and `"akiVersion"`. -Before starting to work on your mod, we suggest you read about Dependency Injection and Inversion of Control as this is the adopted architecture SPT-AKI has adopted. +New to Typescript? Find comprehensive documentation on the [official website](https://www.typescriptlang.org/docs/). -It will be difficult to understand some of the problems you may be having if you dont understand the basics of it. +## **Distribution Guidelines** -A guide explaining all the essentials will be available on the hub on release for you to read about. +Automated tasks are set up to bundle all necessary files for your mod to function in SPT-AKI: -## **Coding:** +> Terminal -> Run Task... -> Show All Tasks... -> npm: build -All your work should be centered around the mod.ts file as an entry point. -You can ONLY change the following properties from the package.json file: `"name"`, `"version"`, `"license"`: `"MIT"`, `"author"`, `"akiVersion"`. +The ZIP output, located in the `dist` directory, contains all required files. Ensure all files are included and modify the `.buildignore` file as needed. This ZIP file is your uploadable asset for the hub. -If you have never used typescript before, you can read about it here: https://www.typescriptlang.org/docs/ +## **Conclusion** -## **Distributing your mod:** +With this setup, you're ready to begin modding with SPT-AKI. If you run into any trouble be sure to check out the [modding documentation on the hub](https://hub.sp-tarkov.com/doc/lexicon/66-modding/). If you really get stuck feel free to join us in the [#mods-development](https://discord.com/channels/875684761291599922/875803116409323562) official Discord channel. -The project has been set up with an automatic task that will copy and zip ALL required files for your mod to work on SPT-AKI. -To run this task you just need to go to: - -> Terminal->Run Task...->Show All Tasks...->npm: build:zip - -The output will be a mod.zip file that will appear on the root of the project. - -Always verify that all files were included into the zip file. \ No newline at end of file +Build something awesome! diff --git a/TypeScript/18CustomItemService/build.mjs b/TypeScript/18CustomItemService/build.mjs new file mode 100644 index 0000000..d00117c --- /dev/null +++ b/TypeScript/18CustomItemService/build.mjs @@ -0,0 +1,383 @@ +#!/usr/bin/env node + +/** + * Build Script + * + * This script automates the build process for server-side SPT mod projects, facilitating the creation of distributable + * mod packages. It performs a series of operations as outlined below: + * - Loads the .buildignore file, which is used to list files that should be ignored during the build process. + * - Loads the package.json to get project details so a descriptive name can be created for the mod package. + * - Creates a distribution directory and a temporary working directory. + * - Copies files to the temporary directory while respecting the .buildignore rules. + * - Creates a zip archive of the project files. + * - Moves the zip file to the root of the distribution directory. + * - Cleans up the temporary directory. + * + * It's typical that this script be customized to suit the needs of each project. For example, the script can be updated + * to perform additional operations, such as moving the mod package to a specific location or uploading it to a server. + * This script is intended to be a starting point for developers to build upon. + * + * Usage: + * - Run this script using npm: `npm run build` + * - Use `npm run buildinfo` for detailed logging. + * + * Note: + * - Ensure that all necessary Node.js modules are installed before running the script: `npm install` + * - The script reads configurations from the `package.json` and `.buildignore` files; ensure they are correctly set up. + * + * @author Refringe + * @version v1.0.0 + */ + +import fs from "fs-extra"; +import os from "os"; +import path from "path"; +import { fileURLToPath } from "url"; +import { dirname } from "path"; +import ignore from "ignore"; +import archiver from "archiver"; +import winston from "winston"; + +// Get the command line arguments to determine whether to use verbose logging. +const args = process.argv.slice(2); +const verbose = args.includes("--verbose") || args.includes("-v"); + +// Configure the Winston logger to use colours. +const logColors = { + error: "red", + warn: "yellow", + info: "grey", + success: "green", +}; +winston.addColors(logColors); + +// Create a logger instance to log build progress. Configure the logger levels to allow for different levels of logging +// based on the verbosity flag, and set the console transport to log messages of the appropriate level. +const logger = winston.createLogger({ + levels: { + error: 0, + warn: 1, + success: 2, + info: 3, + }, + format: winston.format.combine( + winston.format.colorize(), + winston.format.printf(info => { + return `${info.level}: ${info.message}`; + }) + ), + transports: [ + new winston.transports.Console({ + level: verbose ? "info" : "success", + }), + ], +}); + +/** + * The main function orchestrates the build process for creating a distributable mod package. It leverages a series of + * helper functions to perform various tasks such as loading configuration files, setting up directories, copying files + * according to `.buildignore` rules, and creating a ZIP archive of the project files. + * + * Utilizes the Winston logger to provide information on the build status at different stages of the process. + * + * @returns {void} + */ +async function main() { + // Get the current directory where the script is being executed + const currentDir = getCurrentDirectory(); + + // Defining at this scope because we need to use it in the finally block. + let projectDir; + + try { + // Load the .buildignore file to set up an ignore handler for the build process. + const buildIgnorePatterns = await loadBuildIgnoreFile(currentDir); + + // Load the package.json file to get project details. + const packageJson = await loadPackageJson(currentDir); + + // Create a descriptive name for the mod package. + const projectName = createProjectName(packageJson); + logger.log("success", `Project name created: ${projectName}`); + + // Remove the old distribution directory and create a fresh one. + const distDir = await removeOldDistDirectory(currentDir); + logger.log("info", "Distribution directory successfully cleaned."); + + // Create a temporary working directory to perform the build operations. + projectDir = await createTemporaryDirectoryWithProjectName(projectName); + logger.log("success", "Temporary working directory successfully created."); + logger.log("info", projectDir); + + // Copy files to the temporary directory while respecting the .buildignore rules. + logger.log("info", "Beginning copy operation using .buildignore file..."); + await copyFiles(currentDir, projectDir, buildIgnorePatterns); + logger.log("success", "Files successfully copied to temporary directory."); + + // Create a zip archive of the project files. + logger.log("info", "Beginning folder compression..."); + const zipFilePath = path.join(path.dirname(projectDir), `${projectName}.zip`); + await createZipFile(projectDir, zipFilePath, "user/mods/" + projectName); + logger.log("success", "Archive successfully created."); + logger.log("info", zipFilePath); + + // Move the zip file inside of the project directory, within the temporary working directory. + const zipFileInProjectDir = path.join(projectDir, `${projectName}.zip`); + await fs.move(zipFilePath, zipFileInProjectDir); + logger.log("success", "Archive successfully moved."); + logger.log("info", zipFileInProjectDir); + + // Move the temporary directory into the distribution directory. + await fs.move(projectDir, distDir); + logger.log("success", "Temporary directory successfully moved into project distribution directory."); + + // Log the success message. Write out the path to the mod package. + logger.log("success", "------------------------------------"); + logger.log("success", "Build script completed successfully!"); + logger.log("success", "Your mod package has been created in the 'dist' directory:"); + logger.log("success", `/${path.relative(process.cwd(), path.join(distDir, `${projectName}.zip`))}`); + logger.log("success", "------------------------------------"); + if (!verbose) { + logger.log("success", "To see a detailed build log, use `npm run buildinfo`."); + logger.log("success", "------------------------------------"); + } + } catch (err) { + // If any of the file operations fail, log the error. + logger.log("error", "An error occurred: " + err); + } finally { + // Clean up the temporary directory, even if the build fails. + if (projectDir) { + try { + await fs.promises.rm(projectDir, { force: true, recursive: true }); + logger.log("info", "Cleaned temporary directory."); + } catch (err) { + logger.log("error", "Failed to clean temporary directory: " + err); + } + } + } +} + +/** + * Retrieves the current working directory where the script is being executed. This directory is used as a reference + * point for various file operations throughout the build process, ensuring that paths are resolved correctly regardless + * of the location from which the script is invoked. + * + * @returns {string} The absolute path of the current working directory. + */ +function getCurrentDirectory() { + return dirname(fileURLToPath(import.meta.url)); +} + +/** + * Loads the `.buildignore` file and sets up an ignore handler using the `ignore` module. The `.buildignore` file + * contains a list of patterns describing files and directories that should be ignored during the build process. The + * ignore handler created by this method is used to filter files and directories when copying them to the temporary + * directory, ensuring that only necessary files are included in the final mod package. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to an ignore handler. + */ +async function loadBuildIgnoreFile(currentDir) { + const buildIgnorePath = path.join(currentDir, ".buildignore"); + + try { + // Attempt to read the contents of the .buildignore file asynchronously. + const fileContent = await fs.promises.readFile(buildIgnorePath, "utf-8"); + + // Return a new ignore instance and add the rules from the .buildignore file (split by newlines). + return ignore().add(fileContent.split("\n")); + } catch (err) { + logger.log("warn", "Failed to read .buildignore file. No files or directories will be ignored."); + + // Return an empty ignore instance, ensuring the build process can continue. + return ignore(); + } +} + +/** + * Loads the `package.json` file and returns its content as a JSON object. The `package.json` file contains important + * project details such as the name and version, which are used in later stages of the build process to create a + * descriptive name for the mod package. The method reads the file from the current working directory, ensuring that it + * accurately reflects the current state of the project. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to a JSON object containing the contents of the `package.json`. + */ +async function loadPackageJson(currentDir) { + const packageJsonPath = path.join(currentDir, "package.json"); + + // Read the contents of the package.json file asynchronously as a UTF-8 string. + const packageJsonContent = await fs.promises.readFile(packageJsonPath, "utf-8"); + + return JSON.parse(packageJsonContent); +} + +/** + * Constructs a descriptive name for the mod package using details from the `package.json` file. The name is created by + * concatenating the project name, version, and a timestamp, resulting in a unique and descriptive file name for each + * build. This name is used as the base name for the temporary working directory and the final ZIP archive, helping to + * identify different versions of the mod package easily. + * + * @param {Object} packageJson - A JSON object containing the contents of the `package.json` file. + * @returns {string} A string representing the constructed project name. + */ +function createProjectName(packageJson) { + // Remove any non-alphanumeric characters from the author and name. + const author = packageJson.author.replace(/\W/g, ""); + const name = packageJson.name.replace(/\W/g, ""); + const version = packageJson.version; + + // Ensure the name is lowercase, as per the package.json specification. + return `${author}-${name}-${version}`.toLowerCase(); +} + +/** + * Defines the location of the distribution directory where the final mod package will be stored and deletes any + * existing distribution directory to ensure a clean slate for the build process. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to the absolute path to the distribution directory. + */ +async function removeOldDistDirectory(projectDir) { + const distPath = path.join(projectDir, "dist"); + await fs.remove(distPath); + return distPath; +} + +/** + * Creates a temporary working directory using the project name. This directory serves as a staging area where project + * files are gathered before being archived into the final mod package. The method constructs a unique directory path + * by appending the project name to a base temporary directory path, ensuring that each build has its own isolated + * working space. This approach facilitates clean and organized build processes, avoiding potential conflicts with other + * builds. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @param {string} projectName - The constructed project name, used to create a unique path for the temporary directory. + * @returns {Promise} A promise that resolves to the absolute path of the newly created temporary directory. + */ +async function createTemporaryDirectoryWithProjectName(projectName) { + // Create a new directory in the system's temporary folder to hold the project files. + const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "spt-mod-build-")); + + // Create a subdirectory within the temporary directory using the project name for this specific build. + const projectDir = path.join(tempDir, projectName); + await fs.ensureDir(projectDir); + + return projectDir; +} + +/** + * Copies the project files to the temporary directory while respecting the rules defined in the `.buildignore` file. + * The method is recursive, iterating over all files and directories in the source directory and using the ignore + * handler to filter out files and directories that match the patterns defined in the `.buildignore` file. This ensures + * that only the necessary files are included in the final mod package, adhering to the specifications defined by the + * developer in the `.buildignore` file. + * + * The copy operations are delayed and executed in parallel to improve efficiency and reduce the build time. This is + * achieved by creating an array of copy promises and awaiting them all at the end of the function. + * + * @param {string} sourceDirectory - The absolute path of the current working directory. + * @param {string} destinationDirectory - The absolute path of the temporary directory where the files will be copied. + * @param {Ignore} ignoreHandler - The ignore handler created from the `.buildignore` file. + * @returns {Promise} A promise that resolves when all copy operations are completed successfully. + */ +async function copyFiles(srcDir, destDir, ignoreHandler) { + try { + // Read the contents of the source directory to get a list of entries (files and directories). + const entries = await fs.promises.readdir(srcDir, { withFileTypes: true }); + + // Initialize an array to hold the promises returned by recursive calls to copyFiles and copyFile operations. + const copyOperations = []; + + for (const entry of entries) { + // Define the source and destination paths for each entry. + const srcPath = path.join(srcDir, entry.name); + const destPath = path.join(destDir, entry.name); + + // Get the relative path of the source file to check against the ignore handler. + const relativePath = path.relative(process.cwd(), srcPath); + + // If the ignore handler dictates that this file should be ignored, skip to the next iteration. + if (ignoreHandler.ignores(relativePath)) { + logger.log("info", `Ignored: /${path.relative(process.cwd(), srcPath)}`); + continue; + } + + if (entry.isDirectory()) { + // If the entry is a directory, create the corresponding temporary directory and make a recursive call + // to copyFiles to handle copying the contents of the directory. + await fs.ensureDir(destPath); + copyOperations.push(copyFiles(srcPath, destPath, ignoreHandler)); + } else { + // If the entry is a file, add a copyFile operation to the copyOperations array and log the event when + // the operation is successful. + copyOperations.push( + fs.copy(srcPath, destPath).then(() => { + logger.log("info", `Copied: /${path.relative(process.cwd(), srcPath)}`); + }) + ); + } + } + + // Await all copy operations to ensure all files and directories are copied before exiting the function. + await Promise.all(copyOperations); + } catch (err) { + // Log an error message if any error occurs during the copy process. + logger.log("error", "Error copying files: " + err); + } +} + +/** + * Creates a ZIP archive of the project files located in the temporary directory. The method uses the `archiver` module + * to create a ZIP file, which includes all the files that have been copied to the temporary directory during the build + * process. The ZIP file is named using the project name, helping to identify the contents of the archive easily. + * + * @param {string} directoryPath - The absolute path of the temporary directory containing the project files. + * @param {string} projectName - The constructed project name, used to name the ZIP file. + * @returns {Promise} A promise that resolves to the absolute path of the created ZIP file. + */ +async function createZipFile(directoryToZip, zipFilePath, containerDirName) { + return new Promise((resolve, reject) => { + // Create a write stream to the specified ZIP file path. + const output = fs.createWriteStream(zipFilePath); + + // Create a new archiver instance with ZIP format and maximum compression level. + const archive = archiver("zip", { + zlib: { level: 9 }, + }); + + // Set up an event listener for the 'close' event to resolve the promise when the archiver has finalized. + output.on("close", function () { + logger.log("info", "Archiver has finalized. The output and the file descriptor have closed."); + resolve(); + }); + + // Set up an event listener for the 'warning' event to handle warnings appropriately, logging them or rejecting + // the promise based on the error code. + archive.on("warning", function (err) { + if (err.code === "ENOENT") { + logger.log("warn", `Archiver issued a warning: ${err.code} - ${err.message}`); + } else { + reject(err); + } + }); + + // Set up an event listener for the 'error' event to reject the promise if any error occurs during archiving. + archive.on("error", function (err) { + reject(err); + }); + + // Pipe archive data to the file. + archive.pipe(output); + + // Add the directory to the archive, under the provided directory name. + archive.directory(directoryToZip, containerDirName); + + // Finalize the archive, indicating that no more files will be added and triggering the 'close' event once all + // data has been written. + archive.finalize(); + }); +} + +// Engage! +main(); diff --git a/TypeScript/18CustomItemService/package.json b/TypeScript/18CustomItemService/package.json index 45145a8..3d4e038 100644 --- a/TypeScript/18CustomItemService/package.json +++ b/TypeScript/18CustomItemService/package.json @@ -7,17 +7,20 @@ "akiVersion": "~3.7", "scripts": { "setup": "npm i", - "build": "node ./packageBuild.ts" + "build": "node ./build.mjs", + "buildinfo": "node ./build.mjs --verbose" }, "devDependencies": { "@types/node": "20.4.5", "@typescript-eslint/eslint-plugin": "6.2.0", "@typescript-eslint/parser": "6.2.0", - "bestzip": "2.2.1", + "archiver": "^6.0", "eslint": "8.46.0", - "fs-extra": "11.1.1", - "glob": "10.3.3", + "fs-extra": "^11.1", + "ignore": "^5.2", + "os": "^0.1", "tsyringe": "4.8.0", - "typescript": "5.1.6" + "typescript": "5.1.6", + "winston": "^3.9" } -} \ No newline at end of file +} diff --git a/TypeScript/18CustomItemService/packageBuild.ts b/TypeScript/18CustomItemService/packageBuild.ts deleted file mode 100644 index d76ddd8..0000000 --- a/TypeScript/18CustomItemService/packageBuild.ts +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env node - -// This is a simple script used to build a mod package. The script will copy necessary files to the build directory -// and compress the build directory into a zip file that can be easily shared. - -const fs = require("fs-extra"); -const glob = require("glob"); -const zip = require('bestzip'); -const path = require("path"); -const minimatch = require('minimatch'); - -// Load the package.json file to get some information about the package so we can name things appropriately. This is -// atypical, and you would never do this in a production environment, but this script is only used for development so -// it's fine in this case. Some of these values are stored in environment variables, but those differ between node -// versions; the 'author' value is not available after node v14. -const { author, name:packageName, version } = require("./package.json"); - -// Generate the name of the package, stripping out all non-alphanumeric characters in the 'author' and 'name'. -const modName = `${author.replace(/[^a-z0-9]/gi, "")}-${packageName.replace(/[^a-z0-9]/gi, "")}-${version}`; -console.log(`Generated package name: ${modName}`); - -// Delete the old build directory and compressed package file. -fs.rmSync(`${__dirname}/dist`, { force: true, recursive: true }); -console.log("Previous build files deleted."); - -// Generate a list of files that should not be copied over into the distribution directory. This is a blacklist to ensure -// we always copy over additional files and directories that authors may have added to their project. This may need to be -// expanded upon by the mod author to allow for node modules that are used within the mod; example commented out below. -const ignoreList = [ - "node_modules/", - // "node_modules/!(weighted|glob)", // Instead of excluding the entire node_modules directory, allow two node modules. - "src/**/*.js", - "types/", - ".git/", - ".gitea/", - ".eslintignore", - ".eslintrc.json", - ".gitignore", - ".DS_Store", - "packageBuild.ts", - "mod.code-workspace", - "package-lock.json", - "tsconfig.json" -]; -const exclude = glob.sync(`{${ignoreList.join(",")}}`, { realpath: true, dot: true }); - -fs.copySync(__dirname, path.normalize(`${__dirname}/../~${modName}`), {filter: (src) => -{ - const relativePath = path.relative(__dirname, src); - const shouldExclude = exclude.some((pattern) => minimatch(relativePath, pattern)); - console.log(`${relativePath} - Excluded: ${shouldExclude}`); - return !shouldExclude; -},}); - -fs.moveSync(path.normalize(`${__dirname}/../~${modName}`), path.normalize(`${__dirname}/${modName}`), { overwrite: true }); -fs.copySync(path.normalize(`${__dirname}/${modName}`), path.normalize(`${__dirname}/dist`)); -console.log("Build files copied."); - -// Compress the files for easy distribution. The compressed file is saved into the dist directory. When uncompressed we -// need to be sure that it includes a directory that the user can easily copy into their game mods directory. -zip({ - source: modName, - destination: `dist/${modName}.zip`, - cwd: __dirname -}).catch(function(err) -{ - console.error("A bestzip error has occurred: ", err.stack); -}).then(function() -{ - console.log(`Compressed mod package to: /dist/${modName}.zip`); - - // Now that we're done with the compression we can delete the temporary build directory. - fs.rmSync(`${__dirname}/${modName}`, { force: true, recursive: true }); - console.log("Build successful! your zip file has been created and is ready to be uploaded to hub.sp-tarkov.com/files/"); -}); \ No newline at end of file diff --git a/TypeScript/1LogToConsole/.buildignore b/TypeScript/1LogToConsole/.buildignore new file mode 100644 index 0000000..2cbde65 --- /dev/null +++ b/TypeScript/1LogToConsole/.buildignore @@ -0,0 +1,20 @@ +/.buildignore +/.DS_Store +/.editorconfig +/.eslintignore +/.eslintrc.json +/.git +/.github +/.gitignore +/.gitlab +/.nvmrc +/.prettierrc +/.vscode +/build.mjs +/dist +/images +/mod.code-workspace +/node_modules +/package-lock.json +/tsconfig.json +/types diff --git a/TypeScript/1LogToConsole/.eslintrc.json b/TypeScript/1LogToConsole/.eslintrc.json index c505160..071a313 100644 --- a/TypeScript/1LogToConsole/.eslintrc.json +++ b/TypeScript/1LogToConsole/.eslintrc.json @@ -1,75 +1,98 @@ { - "root": true, - "parser": "@typescript-eslint/parser", - "plugins": [ - "@typescript-eslint" - ], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended" - ], - "rules": { - "@typescript-eslint/no-explicit-any": 0, - "@typescript-eslint/no-unused-vars": 1, - "@typescript-eslint/no-empty-interface": 0, - "@typescript-eslint/no-namespace": 0, - "@typescript-eslint/comma-dangle": 1, - "@typescript-eslint/func-call-spacing": 2, - "@typescript-eslint/quotes": 1, - "@typescript-eslint/brace-style": [ - "warn", - "allman" + "root": true, + "parser": "@typescript-eslint/parser", + "plugins": [ + "@typescript-eslint" ], - "@typescript-eslint/naming-convention": [ - "warn", - { - "selector": "default", - "format": ["camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "typeLike", - "format": ["PascalCase"] - }, - { - "selector": "objectLiteralProperty", - "format": ["PascalCase", "camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "typeProperty", - "format": ["PascalCase", "camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "enumMember", - "format": ["UPPER_CASE"] - } + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" ], - "@typescript-eslint/indent": [ - "warn", - 4 - ], - "@typescript-eslint/no-unused-expressions": [ - "warn", - { - "allowShortCircuit": false, - "allowTernary": false - } - ], - "@typescript-eslint/keyword-spacing": [ - "warn", - { - "before": true, - "after": true - } - ], - "@typescript-eslint/explicit-module-boundary-types": [ - "warn", - { - "allowArgumentsExplicitlyTypedAsAny": true - } + "rules": { + "@typescript-eslint/no-explicit-any": 0, + "@typescript-eslint/no-unused-vars": 1, + "@typescript-eslint/no-empty-interface": 0, + "@typescript-eslint/no-namespace": 0, + "@typescript-eslint/comma-dangle": 1, + "@typescript-eslint/func-call-spacing": 2, + "@typescript-eslint/quotes": 1, + "@typescript-eslint/brace-style": [ + "warn", + "allman" + ], + "@typescript-eslint/naming-convention": [ + "warn", + { + "selector": "default", + "format": [ + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "typeLike", + "format": [ + "PascalCase" + ] + }, + { + "selector": "objectLiteralProperty", + "format": [ + "PascalCase", + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "typeProperty", + "format": [ + "PascalCase", + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "enumMember", + "format": [ + "UPPER_CASE" + ] + } + ], + "@typescript-eslint/indent": [ + "warn", + 4 + ], + "@typescript-eslint/no-unused-expressions": [ + "warn", + { + "allowShortCircuit": false, + "allowTernary": false + } + ], + "@typescript-eslint/keyword-spacing": [ + "warn", + { + "before": true, + "after": true + } + ], + "@typescript-eslint/explicit-module-boundary-types": [ + "warn", + { + "allowArgumentsExplicitlyTypedAsAny": true + } + ] + }, + "overrides": [ + { + "files": [ + "*.mjs", + "*.ts" + ], + "env": { + "node": true + } + } ] - } } \ No newline at end of file diff --git a/TypeScript/1LogToConsole/README.md b/TypeScript/1LogToConsole/README.md index 434d063..8cba28e 100644 --- a/TypeScript/1LogToConsole/README.md +++ b/TypeScript/1LogToConsole/README.md @@ -1,64 +1,66 @@ +# Welcome to the SPT-AKI Modding Project -This project was created to automate most parts of building and setting up an environment. +This project is designed to streamline the initial setup process for building and creating mods in the SPT-AKI environment. Follow this guide to set up your environment efficiently. -## **NodeJS:** +## **Table of Contents** +- [NodeJS Setup](#nodejs-setup) +- [IDE Setup](#ide-setup) +- [Workspace Configuration](#workspace-configuration) +- [Environment Setup](#environment-setup) +- [Essential Concepts](#essential-concepts) +- [Coding Guidelines](#coding-guidelines) +- [Distribution Guidelines](#distribution-guidelines) -The first step would be to install nodejs on your pc, the version you NEED is **16.17.1** +## **NodeJS Setup** -That version is the one that has been used to test the mod templates and build scripts. +Before you begin, ensure to install NodeJS version `v16.17.1`, which has been tested thoroughly with our mod templates and build scripts. Download it from the [official NodeJS website](https://nodejs.org/). -It can be downloaded from here: https://nodejs.org/dist/v16.17.1/node-v16.17.1-x64.msi +After installation, it's advised to reboot your system. -A system reboot may be needed after install. +## **IDE Setup** -## **IDE:** +For this project, you can work with either [VSCodium](https://vscodium.com/) or [VSCode](https://code.visualstudio.com/). However, we strongly recommend using VSCode, as all development and testing have been carried out using this IDE, ensuring a smoother experience and compatibility with the project setups. Either way, we have a prepared a workspace file to assist you in setting up your environment. -The second step is having an IDE ready. We've setup a VSCodium workspace file to help with this. +## **Workspace Configuration** -You CAN use Visual Studio Code if you so desire, just keep in mind that our devs tests on the mod files was done using VSCode. +With NodeJS and your chosen IDE ready, initiate the `mod.code-workspace` file using your IDE: -You can get VSCode here: https://code.visualstudio.com/ +> File -> Open Workspace from File... -## **Workspace:** +Upon project loading, consider installing recommended plugins like the ESLint plugin. -Once you have NodeJS and VSCode/VSCodium ready, open the mod.code-workspace file with VSCode (File->Open Workspace from File...). +## **Environment Setup** -Once the project loads you may be recommended to install the ESLint plugin. This is HIGHLY recommended. +An automated task is available to configure your environment for Typescript utilization: -## **Environment Setup:** +> Terminal -> Run Task... -> Show All Tasks... -> npm: install -There is a task that will automatically setup your environment to use typescript. +Note: Preserve the `node_modules` folder as it contains necessary dependencies for Typescript and other functionalities. -To run it, you just need to go to: +## **Essential Concepts** -> Terminal->Run Task...->Show All Tasks...->npm: install +Prioritize understanding Dependency Injection and Inversion of Control, the architectural principles SPT-AKI adopts. Comprehensive guidelines will be available on the hub upon release. -After running this task, your environment will be ready to start coding. +Some resources to get you started: + - [A quick intro to Dependency Injection](https://www.freecodecamp.org/news/a-quick-intro-to-dependency-injection-what-it-is-and-when-to-use-it-7578c84fa88f/) + - [Understanding Inversion of Control (IoC) Principle](https://medium.com/@amitkma/understanding-inversion-of-control-ioc-principle-163b1dc97454) -DO NOT remove the node_modules folder, this is an auto generated directory that has the required dependencies to be able to use typescript and more. +## **Coding Guidelines** -## **IMPORTANT:** +Focus your mod development around the `mod.ts` file. In the `package.json` file, only alter these properties: `"name"`, `"version"`, `"license"`, `"author"`, and `"akiVersion"`. -Before starting to work on your mod, we suggest you read about Dependency Injection and Inversion of Control as this is the adopted architecture SPT-AKI has adopted. +New to Typescript? Find comprehensive documentation on the [official website](https://www.typescriptlang.org/docs/). -It will be difficult to understand some of the problems you may be having if you dont understand the basics of it. +## **Distribution Guidelines** -A guide explaining all the essentials will be available on the hub on release for you to read about. +Automated tasks are set up to bundle all necessary files for your mod to function in SPT-AKI: -## **Coding:** +> Terminal -> Run Task... -> Show All Tasks... -> npm: build -All your work should be centered around the mod.ts file as an entry point. -You can ONLY change the following properties from the package.json file: `"name"`, `"version"`, `"license"`: `"MIT"`, `"author"`, `"akiVersion"`. +The ZIP output, located in the `dist` directory, contains all required files. Ensure all files are included and modify the `.buildignore` file as needed. This ZIP file is your uploadable asset for the hub. -If you have never used typescript before, you can read about it here: https://www.typescriptlang.org/docs/ +## **Conclusion** -## **Distributing your mod:** +With this setup, you're ready to begin modding with SPT-AKI. If you run into any trouble be sure to check out the [modding documentation on the hub](https://hub.sp-tarkov.com/doc/lexicon/66-modding/). If you really get stuck feel free to join us in the [#mods-development](https://discord.com/channels/875684761291599922/875803116409323562) official Discord channel. -The project has been set up with an automatic task that will copy and zip ALL required files for your mod to work on SPT-AKI. -To run this task you just need to go to: - -> Terminal->Run Task...->Show All Tasks...->npm: build:zip - -The output will be a mod.zip file that will appear on the root of the project. - -Always verify that all files were included into the zip file. \ No newline at end of file +Build something awesome! diff --git a/TypeScript/1LogToConsole/build.mjs b/TypeScript/1LogToConsole/build.mjs new file mode 100644 index 0000000..d00117c --- /dev/null +++ b/TypeScript/1LogToConsole/build.mjs @@ -0,0 +1,383 @@ +#!/usr/bin/env node + +/** + * Build Script + * + * This script automates the build process for server-side SPT mod projects, facilitating the creation of distributable + * mod packages. It performs a series of operations as outlined below: + * - Loads the .buildignore file, which is used to list files that should be ignored during the build process. + * - Loads the package.json to get project details so a descriptive name can be created for the mod package. + * - Creates a distribution directory and a temporary working directory. + * - Copies files to the temporary directory while respecting the .buildignore rules. + * - Creates a zip archive of the project files. + * - Moves the zip file to the root of the distribution directory. + * - Cleans up the temporary directory. + * + * It's typical that this script be customized to suit the needs of each project. For example, the script can be updated + * to perform additional operations, such as moving the mod package to a specific location or uploading it to a server. + * This script is intended to be a starting point for developers to build upon. + * + * Usage: + * - Run this script using npm: `npm run build` + * - Use `npm run buildinfo` for detailed logging. + * + * Note: + * - Ensure that all necessary Node.js modules are installed before running the script: `npm install` + * - The script reads configurations from the `package.json` and `.buildignore` files; ensure they are correctly set up. + * + * @author Refringe + * @version v1.0.0 + */ + +import fs from "fs-extra"; +import os from "os"; +import path from "path"; +import { fileURLToPath } from "url"; +import { dirname } from "path"; +import ignore from "ignore"; +import archiver from "archiver"; +import winston from "winston"; + +// Get the command line arguments to determine whether to use verbose logging. +const args = process.argv.slice(2); +const verbose = args.includes("--verbose") || args.includes("-v"); + +// Configure the Winston logger to use colours. +const logColors = { + error: "red", + warn: "yellow", + info: "grey", + success: "green", +}; +winston.addColors(logColors); + +// Create a logger instance to log build progress. Configure the logger levels to allow for different levels of logging +// based on the verbosity flag, and set the console transport to log messages of the appropriate level. +const logger = winston.createLogger({ + levels: { + error: 0, + warn: 1, + success: 2, + info: 3, + }, + format: winston.format.combine( + winston.format.colorize(), + winston.format.printf(info => { + return `${info.level}: ${info.message}`; + }) + ), + transports: [ + new winston.transports.Console({ + level: verbose ? "info" : "success", + }), + ], +}); + +/** + * The main function orchestrates the build process for creating a distributable mod package. It leverages a series of + * helper functions to perform various tasks such as loading configuration files, setting up directories, copying files + * according to `.buildignore` rules, and creating a ZIP archive of the project files. + * + * Utilizes the Winston logger to provide information on the build status at different stages of the process. + * + * @returns {void} + */ +async function main() { + // Get the current directory where the script is being executed + const currentDir = getCurrentDirectory(); + + // Defining at this scope because we need to use it in the finally block. + let projectDir; + + try { + // Load the .buildignore file to set up an ignore handler for the build process. + const buildIgnorePatterns = await loadBuildIgnoreFile(currentDir); + + // Load the package.json file to get project details. + const packageJson = await loadPackageJson(currentDir); + + // Create a descriptive name for the mod package. + const projectName = createProjectName(packageJson); + logger.log("success", `Project name created: ${projectName}`); + + // Remove the old distribution directory and create a fresh one. + const distDir = await removeOldDistDirectory(currentDir); + logger.log("info", "Distribution directory successfully cleaned."); + + // Create a temporary working directory to perform the build operations. + projectDir = await createTemporaryDirectoryWithProjectName(projectName); + logger.log("success", "Temporary working directory successfully created."); + logger.log("info", projectDir); + + // Copy files to the temporary directory while respecting the .buildignore rules. + logger.log("info", "Beginning copy operation using .buildignore file..."); + await copyFiles(currentDir, projectDir, buildIgnorePatterns); + logger.log("success", "Files successfully copied to temporary directory."); + + // Create a zip archive of the project files. + logger.log("info", "Beginning folder compression..."); + const zipFilePath = path.join(path.dirname(projectDir), `${projectName}.zip`); + await createZipFile(projectDir, zipFilePath, "user/mods/" + projectName); + logger.log("success", "Archive successfully created."); + logger.log("info", zipFilePath); + + // Move the zip file inside of the project directory, within the temporary working directory. + const zipFileInProjectDir = path.join(projectDir, `${projectName}.zip`); + await fs.move(zipFilePath, zipFileInProjectDir); + logger.log("success", "Archive successfully moved."); + logger.log("info", zipFileInProjectDir); + + // Move the temporary directory into the distribution directory. + await fs.move(projectDir, distDir); + logger.log("success", "Temporary directory successfully moved into project distribution directory."); + + // Log the success message. Write out the path to the mod package. + logger.log("success", "------------------------------------"); + logger.log("success", "Build script completed successfully!"); + logger.log("success", "Your mod package has been created in the 'dist' directory:"); + logger.log("success", `/${path.relative(process.cwd(), path.join(distDir, `${projectName}.zip`))}`); + logger.log("success", "------------------------------------"); + if (!verbose) { + logger.log("success", "To see a detailed build log, use `npm run buildinfo`."); + logger.log("success", "------------------------------------"); + } + } catch (err) { + // If any of the file operations fail, log the error. + logger.log("error", "An error occurred: " + err); + } finally { + // Clean up the temporary directory, even if the build fails. + if (projectDir) { + try { + await fs.promises.rm(projectDir, { force: true, recursive: true }); + logger.log("info", "Cleaned temporary directory."); + } catch (err) { + logger.log("error", "Failed to clean temporary directory: " + err); + } + } + } +} + +/** + * Retrieves the current working directory where the script is being executed. This directory is used as a reference + * point for various file operations throughout the build process, ensuring that paths are resolved correctly regardless + * of the location from which the script is invoked. + * + * @returns {string} The absolute path of the current working directory. + */ +function getCurrentDirectory() { + return dirname(fileURLToPath(import.meta.url)); +} + +/** + * Loads the `.buildignore` file and sets up an ignore handler using the `ignore` module. The `.buildignore` file + * contains a list of patterns describing files and directories that should be ignored during the build process. The + * ignore handler created by this method is used to filter files and directories when copying them to the temporary + * directory, ensuring that only necessary files are included in the final mod package. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to an ignore handler. + */ +async function loadBuildIgnoreFile(currentDir) { + const buildIgnorePath = path.join(currentDir, ".buildignore"); + + try { + // Attempt to read the contents of the .buildignore file asynchronously. + const fileContent = await fs.promises.readFile(buildIgnorePath, "utf-8"); + + // Return a new ignore instance and add the rules from the .buildignore file (split by newlines). + return ignore().add(fileContent.split("\n")); + } catch (err) { + logger.log("warn", "Failed to read .buildignore file. No files or directories will be ignored."); + + // Return an empty ignore instance, ensuring the build process can continue. + return ignore(); + } +} + +/** + * Loads the `package.json` file and returns its content as a JSON object. The `package.json` file contains important + * project details such as the name and version, which are used in later stages of the build process to create a + * descriptive name for the mod package. The method reads the file from the current working directory, ensuring that it + * accurately reflects the current state of the project. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to a JSON object containing the contents of the `package.json`. + */ +async function loadPackageJson(currentDir) { + const packageJsonPath = path.join(currentDir, "package.json"); + + // Read the contents of the package.json file asynchronously as a UTF-8 string. + const packageJsonContent = await fs.promises.readFile(packageJsonPath, "utf-8"); + + return JSON.parse(packageJsonContent); +} + +/** + * Constructs a descriptive name for the mod package using details from the `package.json` file. The name is created by + * concatenating the project name, version, and a timestamp, resulting in a unique and descriptive file name for each + * build. This name is used as the base name for the temporary working directory and the final ZIP archive, helping to + * identify different versions of the mod package easily. + * + * @param {Object} packageJson - A JSON object containing the contents of the `package.json` file. + * @returns {string} A string representing the constructed project name. + */ +function createProjectName(packageJson) { + // Remove any non-alphanumeric characters from the author and name. + const author = packageJson.author.replace(/\W/g, ""); + const name = packageJson.name.replace(/\W/g, ""); + const version = packageJson.version; + + // Ensure the name is lowercase, as per the package.json specification. + return `${author}-${name}-${version}`.toLowerCase(); +} + +/** + * Defines the location of the distribution directory where the final mod package will be stored and deletes any + * existing distribution directory to ensure a clean slate for the build process. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to the absolute path to the distribution directory. + */ +async function removeOldDistDirectory(projectDir) { + const distPath = path.join(projectDir, "dist"); + await fs.remove(distPath); + return distPath; +} + +/** + * Creates a temporary working directory using the project name. This directory serves as a staging area where project + * files are gathered before being archived into the final mod package. The method constructs a unique directory path + * by appending the project name to a base temporary directory path, ensuring that each build has its own isolated + * working space. This approach facilitates clean and organized build processes, avoiding potential conflicts with other + * builds. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @param {string} projectName - The constructed project name, used to create a unique path for the temporary directory. + * @returns {Promise} A promise that resolves to the absolute path of the newly created temporary directory. + */ +async function createTemporaryDirectoryWithProjectName(projectName) { + // Create a new directory in the system's temporary folder to hold the project files. + const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "spt-mod-build-")); + + // Create a subdirectory within the temporary directory using the project name for this specific build. + const projectDir = path.join(tempDir, projectName); + await fs.ensureDir(projectDir); + + return projectDir; +} + +/** + * Copies the project files to the temporary directory while respecting the rules defined in the `.buildignore` file. + * The method is recursive, iterating over all files and directories in the source directory and using the ignore + * handler to filter out files and directories that match the patterns defined in the `.buildignore` file. This ensures + * that only the necessary files are included in the final mod package, adhering to the specifications defined by the + * developer in the `.buildignore` file. + * + * The copy operations are delayed and executed in parallel to improve efficiency and reduce the build time. This is + * achieved by creating an array of copy promises and awaiting them all at the end of the function. + * + * @param {string} sourceDirectory - The absolute path of the current working directory. + * @param {string} destinationDirectory - The absolute path of the temporary directory where the files will be copied. + * @param {Ignore} ignoreHandler - The ignore handler created from the `.buildignore` file. + * @returns {Promise} A promise that resolves when all copy operations are completed successfully. + */ +async function copyFiles(srcDir, destDir, ignoreHandler) { + try { + // Read the contents of the source directory to get a list of entries (files and directories). + const entries = await fs.promises.readdir(srcDir, { withFileTypes: true }); + + // Initialize an array to hold the promises returned by recursive calls to copyFiles and copyFile operations. + const copyOperations = []; + + for (const entry of entries) { + // Define the source and destination paths for each entry. + const srcPath = path.join(srcDir, entry.name); + const destPath = path.join(destDir, entry.name); + + // Get the relative path of the source file to check against the ignore handler. + const relativePath = path.relative(process.cwd(), srcPath); + + // If the ignore handler dictates that this file should be ignored, skip to the next iteration. + if (ignoreHandler.ignores(relativePath)) { + logger.log("info", `Ignored: /${path.relative(process.cwd(), srcPath)}`); + continue; + } + + if (entry.isDirectory()) { + // If the entry is a directory, create the corresponding temporary directory and make a recursive call + // to copyFiles to handle copying the contents of the directory. + await fs.ensureDir(destPath); + copyOperations.push(copyFiles(srcPath, destPath, ignoreHandler)); + } else { + // If the entry is a file, add a copyFile operation to the copyOperations array and log the event when + // the operation is successful. + copyOperations.push( + fs.copy(srcPath, destPath).then(() => { + logger.log("info", `Copied: /${path.relative(process.cwd(), srcPath)}`); + }) + ); + } + } + + // Await all copy operations to ensure all files and directories are copied before exiting the function. + await Promise.all(copyOperations); + } catch (err) { + // Log an error message if any error occurs during the copy process. + logger.log("error", "Error copying files: " + err); + } +} + +/** + * Creates a ZIP archive of the project files located in the temporary directory. The method uses the `archiver` module + * to create a ZIP file, which includes all the files that have been copied to the temporary directory during the build + * process. The ZIP file is named using the project name, helping to identify the contents of the archive easily. + * + * @param {string} directoryPath - The absolute path of the temporary directory containing the project files. + * @param {string} projectName - The constructed project name, used to name the ZIP file. + * @returns {Promise} A promise that resolves to the absolute path of the created ZIP file. + */ +async function createZipFile(directoryToZip, zipFilePath, containerDirName) { + return new Promise((resolve, reject) => { + // Create a write stream to the specified ZIP file path. + const output = fs.createWriteStream(zipFilePath); + + // Create a new archiver instance with ZIP format and maximum compression level. + const archive = archiver("zip", { + zlib: { level: 9 }, + }); + + // Set up an event listener for the 'close' event to resolve the promise when the archiver has finalized. + output.on("close", function () { + logger.log("info", "Archiver has finalized. The output and the file descriptor have closed."); + resolve(); + }); + + // Set up an event listener for the 'warning' event to handle warnings appropriately, logging them or rejecting + // the promise based on the error code. + archive.on("warning", function (err) { + if (err.code === "ENOENT") { + logger.log("warn", `Archiver issued a warning: ${err.code} - ${err.message}`); + } else { + reject(err); + } + }); + + // Set up an event listener for the 'error' event to reject the promise if any error occurs during archiving. + archive.on("error", function (err) { + reject(err); + }); + + // Pipe archive data to the file. + archive.pipe(output); + + // Add the directory to the archive, under the provided directory name. + archive.directory(directoryToZip, containerDirName); + + // Finalize the archive, indicating that no more files will be added and triggering the 'close' event once all + // data has been written. + archive.finalize(); + }); +} + +// Engage! +main(); diff --git a/TypeScript/1LogToConsole/package.json b/TypeScript/1LogToConsole/package.json index d2b2248..a768e49 100644 --- a/TypeScript/1LogToConsole/package.json +++ b/TypeScript/1LogToConsole/package.json @@ -7,17 +7,20 @@ "akiVersion": "~3.7", "scripts": { "setup": "npm i", - "build": "node ./packageBuild.ts" + "build": "node ./build.mjs", + "buildinfo": "node ./build.mjs --verbose" }, "devDependencies": { "@types/node": "20.4.5", "@typescript-eslint/eslint-plugin": "6.2.0", "@typescript-eslint/parser": "6.2.0", - "bestzip": "2.2.1", + "archiver": "^6.0", "eslint": "8.46.0", - "fs-extra": "11.1.1", - "glob": "10.3.3", + "fs-extra": "^11.1", + "ignore": "^5.2", + "os": "^0.1", "tsyringe": "4.8.0", - "typescript": "5.1.6" + "typescript": "5.1.6", + "winston": "^3.9" } -} \ No newline at end of file +} diff --git a/TypeScript/1LogToConsole/packageBuild.ts b/TypeScript/1LogToConsole/packageBuild.ts deleted file mode 100644 index d76ddd8..0000000 --- a/TypeScript/1LogToConsole/packageBuild.ts +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env node - -// This is a simple script used to build a mod package. The script will copy necessary files to the build directory -// and compress the build directory into a zip file that can be easily shared. - -const fs = require("fs-extra"); -const glob = require("glob"); -const zip = require('bestzip'); -const path = require("path"); -const minimatch = require('minimatch'); - -// Load the package.json file to get some information about the package so we can name things appropriately. This is -// atypical, and you would never do this in a production environment, but this script is only used for development so -// it's fine in this case. Some of these values are stored in environment variables, but those differ between node -// versions; the 'author' value is not available after node v14. -const { author, name:packageName, version } = require("./package.json"); - -// Generate the name of the package, stripping out all non-alphanumeric characters in the 'author' and 'name'. -const modName = `${author.replace(/[^a-z0-9]/gi, "")}-${packageName.replace(/[^a-z0-9]/gi, "")}-${version}`; -console.log(`Generated package name: ${modName}`); - -// Delete the old build directory and compressed package file. -fs.rmSync(`${__dirname}/dist`, { force: true, recursive: true }); -console.log("Previous build files deleted."); - -// Generate a list of files that should not be copied over into the distribution directory. This is a blacklist to ensure -// we always copy over additional files and directories that authors may have added to their project. This may need to be -// expanded upon by the mod author to allow for node modules that are used within the mod; example commented out below. -const ignoreList = [ - "node_modules/", - // "node_modules/!(weighted|glob)", // Instead of excluding the entire node_modules directory, allow two node modules. - "src/**/*.js", - "types/", - ".git/", - ".gitea/", - ".eslintignore", - ".eslintrc.json", - ".gitignore", - ".DS_Store", - "packageBuild.ts", - "mod.code-workspace", - "package-lock.json", - "tsconfig.json" -]; -const exclude = glob.sync(`{${ignoreList.join(",")}}`, { realpath: true, dot: true }); - -fs.copySync(__dirname, path.normalize(`${__dirname}/../~${modName}`), {filter: (src) => -{ - const relativePath = path.relative(__dirname, src); - const shouldExclude = exclude.some((pattern) => minimatch(relativePath, pattern)); - console.log(`${relativePath} - Excluded: ${shouldExclude}`); - return !shouldExclude; -},}); - -fs.moveSync(path.normalize(`${__dirname}/../~${modName}`), path.normalize(`${__dirname}/${modName}`), { overwrite: true }); -fs.copySync(path.normalize(`${__dirname}/${modName}`), path.normalize(`${__dirname}/dist`)); -console.log("Build files copied."); - -// Compress the files for easy distribution. The compressed file is saved into the dist directory. When uncompressed we -// need to be sure that it includes a directory that the user can easily copy into their game mods directory. -zip({ - source: modName, - destination: `dist/${modName}.zip`, - cwd: __dirname -}).catch(function(err) -{ - console.error("A bestzip error has occurred: ", err.stack); -}).then(function() -{ - console.log(`Compressed mod package to: /dist/${modName}.zip`); - - // Now that we're done with the compression we can delete the temporary build directory. - fs.rmSync(`${__dirname}/${modName}`, { force: true, recursive: true }); - console.log("Build successful! your zip file has been created and is ready to be uploaded to hub.sp-tarkov.com/files/"); -}); \ No newline at end of file diff --git a/TypeScript/2EditDatabase/.buildignore b/TypeScript/2EditDatabase/.buildignore new file mode 100644 index 0000000..2cbde65 --- /dev/null +++ b/TypeScript/2EditDatabase/.buildignore @@ -0,0 +1,20 @@ +/.buildignore +/.DS_Store +/.editorconfig +/.eslintignore +/.eslintrc.json +/.git +/.github +/.gitignore +/.gitlab +/.nvmrc +/.prettierrc +/.vscode +/build.mjs +/dist +/images +/mod.code-workspace +/node_modules +/package-lock.json +/tsconfig.json +/types diff --git a/TypeScript/2EditDatabase/.eslintrc.json b/TypeScript/2EditDatabase/.eslintrc.json index c505160..071a313 100644 --- a/TypeScript/2EditDatabase/.eslintrc.json +++ b/TypeScript/2EditDatabase/.eslintrc.json @@ -1,75 +1,98 @@ { - "root": true, - "parser": "@typescript-eslint/parser", - "plugins": [ - "@typescript-eslint" - ], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended" - ], - "rules": { - "@typescript-eslint/no-explicit-any": 0, - "@typescript-eslint/no-unused-vars": 1, - "@typescript-eslint/no-empty-interface": 0, - "@typescript-eslint/no-namespace": 0, - "@typescript-eslint/comma-dangle": 1, - "@typescript-eslint/func-call-spacing": 2, - "@typescript-eslint/quotes": 1, - "@typescript-eslint/brace-style": [ - "warn", - "allman" + "root": true, + "parser": "@typescript-eslint/parser", + "plugins": [ + "@typescript-eslint" ], - "@typescript-eslint/naming-convention": [ - "warn", - { - "selector": "default", - "format": ["camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "typeLike", - "format": ["PascalCase"] - }, - { - "selector": "objectLiteralProperty", - "format": ["PascalCase", "camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "typeProperty", - "format": ["PascalCase", "camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "enumMember", - "format": ["UPPER_CASE"] - } + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" ], - "@typescript-eslint/indent": [ - "warn", - 4 - ], - "@typescript-eslint/no-unused-expressions": [ - "warn", - { - "allowShortCircuit": false, - "allowTernary": false - } - ], - "@typescript-eslint/keyword-spacing": [ - "warn", - { - "before": true, - "after": true - } - ], - "@typescript-eslint/explicit-module-boundary-types": [ - "warn", - { - "allowArgumentsExplicitlyTypedAsAny": true - } + "rules": { + "@typescript-eslint/no-explicit-any": 0, + "@typescript-eslint/no-unused-vars": 1, + "@typescript-eslint/no-empty-interface": 0, + "@typescript-eslint/no-namespace": 0, + "@typescript-eslint/comma-dangle": 1, + "@typescript-eslint/func-call-spacing": 2, + "@typescript-eslint/quotes": 1, + "@typescript-eslint/brace-style": [ + "warn", + "allman" + ], + "@typescript-eslint/naming-convention": [ + "warn", + { + "selector": "default", + "format": [ + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "typeLike", + "format": [ + "PascalCase" + ] + }, + { + "selector": "objectLiteralProperty", + "format": [ + "PascalCase", + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "typeProperty", + "format": [ + "PascalCase", + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "enumMember", + "format": [ + "UPPER_CASE" + ] + } + ], + "@typescript-eslint/indent": [ + "warn", + 4 + ], + "@typescript-eslint/no-unused-expressions": [ + "warn", + { + "allowShortCircuit": false, + "allowTernary": false + } + ], + "@typescript-eslint/keyword-spacing": [ + "warn", + { + "before": true, + "after": true + } + ], + "@typescript-eslint/explicit-module-boundary-types": [ + "warn", + { + "allowArgumentsExplicitlyTypedAsAny": true + } + ] + }, + "overrides": [ + { + "files": [ + "*.mjs", + "*.ts" + ], + "env": { + "node": true + } + } ] - } } \ No newline at end of file diff --git a/TypeScript/2EditDatabase/README.md b/TypeScript/2EditDatabase/README.md index 434d063..8cba28e 100644 --- a/TypeScript/2EditDatabase/README.md +++ b/TypeScript/2EditDatabase/README.md @@ -1,64 +1,66 @@ +# Welcome to the SPT-AKI Modding Project -This project was created to automate most parts of building and setting up an environment. +This project is designed to streamline the initial setup process for building and creating mods in the SPT-AKI environment. Follow this guide to set up your environment efficiently. -## **NodeJS:** +## **Table of Contents** +- [NodeJS Setup](#nodejs-setup) +- [IDE Setup](#ide-setup) +- [Workspace Configuration](#workspace-configuration) +- [Environment Setup](#environment-setup) +- [Essential Concepts](#essential-concepts) +- [Coding Guidelines](#coding-guidelines) +- [Distribution Guidelines](#distribution-guidelines) -The first step would be to install nodejs on your pc, the version you NEED is **16.17.1** +## **NodeJS Setup** -That version is the one that has been used to test the mod templates and build scripts. +Before you begin, ensure to install NodeJS version `v16.17.1`, which has been tested thoroughly with our mod templates and build scripts. Download it from the [official NodeJS website](https://nodejs.org/). -It can be downloaded from here: https://nodejs.org/dist/v16.17.1/node-v16.17.1-x64.msi +After installation, it's advised to reboot your system. -A system reboot may be needed after install. +## **IDE Setup** -## **IDE:** +For this project, you can work with either [VSCodium](https://vscodium.com/) or [VSCode](https://code.visualstudio.com/). However, we strongly recommend using VSCode, as all development and testing have been carried out using this IDE, ensuring a smoother experience and compatibility with the project setups. Either way, we have a prepared a workspace file to assist you in setting up your environment. -The second step is having an IDE ready. We've setup a VSCodium workspace file to help with this. +## **Workspace Configuration** -You CAN use Visual Studio Code if you so desire, just keep in mind that our devs tests on the mod files was done using VSCode. +With NodeJS and your chosen IDE ready, initiate the `mod.code-workspace` file using your IDE: -You can get VSCode here: https://code.visualstudio.com/ +> File -> Open Workspace from File... -## **Workspace:** +Upon project loading, consider installing recommended plugins like the ESLint plugin. -Once you have NodeJS and VSCode/VSCodium ready, open the mod.code-workspace file with VSCode (File->Open Workspace from File...). +## **Environment Setup** -Once the project loads you may be recommended to install the ESLint plugin. This is HIGHLY recommended. +An automated task is available to configure your environment for Typescript utilization: -## **Environment Setup:** +> Terminal -> Run Task... -> Show All Tasks... -> npm: install -There is a task that will automatically setup your environment to use typescript. +Note: Preserve the `node_modules` folder as it contains necessary dependencies for Typescript and other functionalities. -To run it, you just need to go to: +## **Essential Concepts** -> Terminal->Run Task...->Show All Tasks...->npm: install +Prioritize understanding Dependency Injection and Inversion of Control, the architectural principles SPT-AKI adopts. Comprehensive guidelines will be available on the hub upon release. -After running this task, your environment will be ready to start coding. +Some resources to get you started: + - [A quick intro to Dependency Injection](https://www.freecodecamp.org/news/a-quick-intro-to-dependency-injection-what-it-is-and-when-to-use-it-7578c84fa88f/) + - [Understanding Inversion of Control (IoC) Principle](https://medium.com/@amitkma/understanding-inversion-of-control-ioc-principle-163b1dc97454) -DO NOT remove the node_modules folder, this is an auto generated directory that has the required dependencies to be able to use typescript and more. +## **Coding Guidelines** -## **IMPORTANT:** +Focus your mod development around the `mod.ts` file. In the `package.json` file, only alter these properties: `"name"`, `"version"`, `"license"`, `"author"`, and `"akiVersion"`. -Before starting to work on your mod, we suggest you read about Dependency Injection and Inversion of Control as this is the adopted architecture SPT-AKI has adopted. +New to Typescript? Find comprehensive documentation on the [official website](https://www.typescriptlang.org/docs/). -It will be difficult to understand some of the problems you may be having if you dont understand the basics of it. +## **Distribution Guidelines** -A guide explaining all the essentials will be available on the hub on release for you to read about. +Automated tasks are set up to bundle all necessary files for your mod to function in SPT-AKI: -## **Coding:** +> Terminal -> Run Task... -> Show All Tasks... -> npm: build -All your work should be centered around the mod.ts file as an entry point. -You can ONLY change the following properties from the package.json file: `"name"`, `"version"`, `"license"`: `"MIT"`, `"author"`, `"akiVersion"`. +The ZIP output, located in the `dist` directory, contains all required files. Ensure all files are included and modify the `.buildignore` file as needed. This ZIP file is your uploadable asset for the hub. -If you have never used typescript before, you can read about it here: https://www.typescriptlang.org/docs/ +## **Conclusion** -## **Distributing your mod:** +With this setup, you're ready to begin modding with SPT-AKI. If you run into any trouble be sure to check out the [modding documentation on the hub](https://hub.sp-tarkov.com/doc/lexicon/66-modding/). If you really get stuck feel free to join us in the [#mods-development](https://discord.com/channels/875684761291599922/875803116409323562) official Discord channel. -The project has been set up with an automatic task that will copy and zip ALL required files for your mod to work on SPT-AKI. -To run this task you just need to go to: - -> Terminal->Run Task...->Show All Tasks...->npm: build:zip - -The output will be a mod.zip file that will appear on the root of the project. - -Always verify that all files were included into the zip file. \ No newline at end of file +Build something awesome! diff --git a/TypeScript/2EditDatabase/build.mjs b/TypeScript/2EditDatabase/build.mjs new file mode 100644 index 0000000..d00117c --- /dev/null +++ b/TypeScript/2EditDatabase/build.mjs @@ -0,0 +1,383 @@ +#!/usr/bin/env node + +/** + * Build Script + * + * This script automates the build process for server-side SPT mod projects, facilitating the creation of distributable + * mod packages. It performs a series of operations as outlined below: + * - Loads the .buildignore file, which is used to list files that should be ignored during the build process. + * - Loads the package.json to get project details so a descriptive name can be created for the mod package. + * - Creates a distribution directory and a temporary working directory. + * - Copies files to the temporary directory while respecting the .buildignore rules. + * - Creates a zip archive of the project files. + * - Moves the zip file to the root of the distribution directory. + * - Cleans up the temporary directory. + * + * It's typical that this script be customized to suit the needs of each project. For example, the script can be updated + * to perform additional operations, such as moving the mod package to a specific location or uploading it to a server. + * This script is intended to be a starting point for developers to build upon. + * + * Usage: + * - Run this script using npm: `npm run build` + * - Use `npm run buildinfo` for detailed logging. + * + * Note: + * - Ensure that all necessary Node.js modules are installed before running the script: `npm install` + * - The script reads configurations from the `package.json` and `.buildignore` files; ensure they are correctly set up. + * + * @author Refringe + * @version v1.0.0 + */ + +import fs from "fs-extra"; +import os from "os"; +import path from "path"; +import { fileURLToPath } from "url"; +import { dirname } from "path"; +import ignore from "ignore"; +import archiver from "archiver"; +import winston from "winston"; + +// Get the command line arguments to determine whether to use verbose logging. +const args = process.argv.slice(2); +const verbose = args.includes("--verbose") || args.includes("-v"); + +// Configure the Winston logger to use colours. +const logColors = { + error: "red", + warn: "yellow", + info: "grey", + success: "green", +}; +winston.addColors(logColors); + +// Create a logger instance to log build progress. Configure the logger levels to allow for different levels of logging +// based on the verbosity flag, and set the console transport to log messages of the appropriate level. +const logger = winston.createLogger({ + levels: { + error: 0, + warn: 1, + success: 2, + info: 3, + }, + format: winston.format.combine( + winston.format.colorize(), + winston.format.printf(info => { + return `${info.level}: ${info.message}`; + }) + ), + transports: [ + new winston.transports.Console({ + level: verbose ? "info" : "success", + }), + ], +}); + +/** + * The main function orchestrates the build process for creating a distributable mod package. It leverages a series of + * helper functions to perform various tasks such as loading configuration files, setting up directories, copying files + * according to `.buildignore` rules, and creating a ZIP archive of the project files. + * + * Utilizes the Winston logger to provide information on the build status at different stages of the process. + * + * @returns {void} + */ +async function main() { + // Get the current directory where the script is being executed + const currentDir = getCurrentDirectory(); + + // Defining at this scope because we need to use it in the finally block. + let projectDir; + + try { + // Load the .buildignore file to set up an ignore handler for the build process. + const buildIgnorePatterns = await loadBuildIgnoreFile(currentDir); + + // Load the package.json file to get project details. + const packageJson = await loadPackageJson(currentDir); + + // Create a descriptive name for the mod package. + const projectName = createProjectName(packageJson); + logger.log("success", `Project name created: ${projectName}`); + + // Remove the old distribution directory and create a fresh one. + const distDir = await removeOldDistDirectory(currentDir); + logger.log("info", "Distribution directory successfully cleaned."); + + // Create a temporary working directory to perform the build operations. + projectDir = await createTemporaryDirectoryWithProjectName(projectName); + logger.log("success", "Temporary working directory successfully created."); + logger.log("info", projectDir); + + // Copy files to the temporary directory while respecting the .buildignore rules. + logger.log("info", "Beginning copy operation using .buildignore file..."); + await copyFiles(currentDir, projectDir, buildIgnorePatterns); + logger.log("success", "Files successfully copied to temporary directory."); + + // Create a zip archive of the project files. + logger.log("info", "Beginning folder compression..."); + const zipFilePath = path.join(path.dirname(projectDir), `${projectName}.zip`); + await createZipFile(projectDir, zipFilePath, "user/mods/" + projectName); + logger.log("success", "Archive successfully created."); + logger.log("info", zipFilePath); + + // Move the zip file inside of the project directory, within the temporary working directory. + const zipFileInProjectDir = path.join(projectDir, `${projectName}.zip`); + await fs.move(zipFilePath, zipFileInProjectDir); + logger.log("success", "Archive successfully moved."); + logger.log("info", zipFileInProjectDir); + + // Move the temporary directory into the distribution directory. + await fs.move(projectDir, distDir); + logger.log("success", "Temporary directory successfully moved into project distribution directory."); + + // Log the success message. Write out the path to the mod package. + logger.log("success", "------------------------------------"); + logger.log("success", "Build script completed successfully!"); + logger.log("success", "Your mod package has been created in the 'dist' directory:"); + logger.log("success", `/${path.relative(process.cwd(), path.join(distDir, `${projectName}.zip`))}`); + logger.log("success", "------------------------------------"); + if (!verbose) { + logger.log("success", "To see a detailed build log, use `npm run buildinfo`."); + logger.log("success", "------------------------------------"); + } + } catch (err) { + // If any of the file operations fail, log the error. + logger.log("error", "An error occurred: " + err); + } finally { + // Clean up the temporary directory, even if the build fails. + if (projectDir) { + try { + await fs.promises.rm(projectDir, { force: true, recursive: true }); + logger.log("info", "Cleaned temporary directory."); + } catch (err) { + logger.log("error", "Failed to clean temporary directory: " + err); + } + } + } +} + +/** + * Retrieves the current working directory where the script is being executed. This directory is used as a reference + * point for various file operations throughout the build process, ensuring that paths are resolved correctly regardless + * of the location from which the script is invoked. + * + * @returns {string} The absolute path of the current working directory. + */ +function getCurrentDirectory() { + return dirname(fileURLToPath(import.meta.url)); +} + +/** + * Loads the `.buildignore` file and sets up an ignore handler using the `ignore` module. The `.buildignore` file + * contains a list of patterns describing files and directories that should be ignored during the build process. The + * ignore handler created by this method is used to filter files and directories when copying them to the temporary + * directory, ensuring that only necessary files are included in the final mod package. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to an ignore handler. + */ +async function loadBuildIgnoreFile(currentDir) { + const buildIgnorePath = path.join(currentDir, ".buildignore"); + + try { + // Attempt to read the contents of the .buildignore file asynchronously. + const fileContent = await fs.promises.readFile(buildIgnorePath, "utf-8"); + + // Return a new ignore instance and add the rules from the .buildignore file (split by newlines). + return ignore().add(fileContent.split("\n")); + } catch (err) { + logger.log("warn", "Failed to read .buildignore file. No files or directories will be ignored."); + + // Return an empty ignore instance, ensuring the build process can continue. + return ignore(); + } +} + +/** + * Loads the `package.json` file and returns its content as a JSON object. The `package.json` file contains important + * project details such as the name and version, which are used in later stages of the build process to create a + * descriptive name for the mod package. The method reads the file from the current working directory, ensuring that it + * accurately reflects the current state of the project. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to a JSON object containing the contents of the `package.json`. + */ +async function loadPackageJson(currentDir) { + const packageJsonPath = path.join(currentDir, "package.json"); + + // Read the contents of the package.json file asynchronously as a UTF-8 string. + const packageJsonContent = await fs.promises.readFile(packageJsonPath, "utf-8"); + + return JSON.parse(packageJsonContent); +} + +/** + * Constructs a descriptive name for the mod package using details from the `package.json` file. The name is created by + * concatenating the project name, version, and a timestamp, resulting in a unique and descriptive file name for each + * build. This name is used as the base name for the temporary working directory and the final ZIP archive, helping to + * identify different versions of the mod package easily. + * + * @param {Object} packageJson - A JSON object containing the contents of the `package.json` file. + * @returns {string} A string representing the constructed project name. + */ +function createProjectName(packageJson) { + // Remove any non-alphanumeric characters from the author and name. + const author = packageJson.author.replace(/\W/g, ""); + const name = packageJson.name.replace(/\W/g, ""); + const version = packageJson.version; + + // Ensure the name is lowercase, as per the package.json specification. + return `${author}-${name}-${version}`.toLowerCase(); +} + +/** + * Defines the location of the distribution directory where the final mod package will be stored and deletes any + * existing distribution directory to ensure a clean slate for the build process. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to the absolute path to the distribution directory. + */ +async function removeOldDistDirectory(projectDir) { + const distPath = path.join(projectDir, "dist"); + await fs.remove(distPath); + return distPath; +} + +/** + * Creates a temporary working directory using the project name. This directory serves as a staging area where project + * files are gathered before being archived into the final mod package. The method constructs a unique directory path + * by appending the project name to a base temporary directory path, ensuring that each build has its own isolated + * working space. This approach facilitates clean and organized build processes, avoiding potential conflicts with other + * builds. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @param {string} projectName - The constructed project name, used to create a unique path for the temporary directory. + * @returns {Promise} A promise that resolves to the absolute path of the newly created temporary directory. + */ +async function createTemporaryDirectoryWithProjectName(projectName) { + // Create a new directory in the system's temporary folder to hold the project files. + const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "spt-mod-build-")); + + // Create a subdirectory within the temporary directory using the project name for this specific build. + const projectDir = path.join(tempDir, projectName); + await fs.ensureDir(projectDir); + + return projectDir; +} + +/** + * Copies the project files to the temporary directory while respecting the rules defined in the `.buildignore` file. + * The method is recursive, iterating over all files and directories in the source directory and using the ignore + * handler to filter out files and directories that match the patterns defined in the `.buildignore` file. This ensures + * that only the necessary files are included in the final mod package, adhering to the specifications defined by the + * developer in the `.buildignore` file. + * + * The copy operations are delayed and executed in parallel to improve efficiency and reduce the build time. This is + * achieved by creating an array of copy promises and awaiting them all at the end of the function. + * + * @param {string} sourceDirectory - The absolute path of the current working directory. + * @param {string} destinationDirectory - The absolute path of the temporary directory where the files will be copied. + * @param {Ignore} ignoreHandler - The ignore handler created from the `.buildignore` file. + * @returns {Promise} A promise that resolves when all copy operations are completed successfully. + */ +async function copyFiles(srcDir, destDir, ignoreHandler) { + try { + // Read the contents of the source directory to get a list of entries (files and directories). + const entries = await fs.promises.readdir(srcDir, { withFileTypes: true }); + + // Initialize an array to hold the promises returned by recursive calls to copyFiles and copyFile operations. + const copyOperations = []; + + for (const entry of entries) { + // Define the source and destination paths for each entry. + const srcPath = path.join(srcDir, entry.name); + const destPath = path.join(destDir, entry.name); + + // Get the relative path of the source file to check against the ignore handler. + const relativePath = path.relative(process.cwd(), srcPath); + + // If the ignore handler dictates that this file should be ignored, skip to the next iteration. + if (ignoreHandler.ignores(relativePath)) { + logger.log("info", `Ignored: /${path.relative(process.cwd(), srcPath)}`); + continue; + } + + if (entry.isDirectory()) { + // If the entry is a directory, create the corresponding temporary directory and make a recursive call + // to copyFiles to handle copying the contents of the directory. + await fs.ensureDir(destPath); + copyOperations.push(copyFiles(srcPath, destPath, ignoreHandler)); + } else { + // If the entry is a file, add a copyFile operation to the copyOperations array and log the event when + // the operation is successful. + copyOperations.push( + fs.copy(srcPath, destPath).then(() => { + logger.log("info", `Copied: /${path.relative(process.cwd(), srcPath)}`); + }) + ); + } + } + + // Await all copy operations to ensure all files and directories are copied before exiting the function. + await Promise.all(copyOperations); + } catch (err) { + // Log an error message if any error occurs during the copy process. + logger.log("error", "Error copying files: " + err); + } +} + +/** + * Creates a ZIP archive of the project files located in the temporary directory. The method uses the `archiver` module + * to create a ZIP file, which includes all the files that have been copied to the temporary directory during the build + * process. The ZIP file is named using the project name, helping to identify the contents of the archive easily. + * + * @param {string} directoryPath - The absolute path of the temporary directory containing the project files. + * @param {string} projectName - The constructed project name, used to name the ZIP file. + * @returns {Promise} A promise that resolves to the absolute path of the created ZIP file. + */ +async function createZipFile(directoryToZip, zipFilePath, containerDirName) { + return new Promise((resolve, reject) => { + // Create a write stream to the specified ZIP file path. + const output = fs.createWriteStream(zipFilePath); + + // Create a new archiver instance with ZIP format and maximum compression level. + const archive = archiver("zip", { + zlib: { level: 9 }, + }); + + // Set up an event listener for the 'close' event to resolve the promise when the archiver has finalized. + output.on("close", function () { + logger.log("info", "Archiver has finalized. The output and the file descriptor have closed."); + resolve(); + }); + + // Set up an event listener for the 'warning' event to handle warnings appropriately, logging them or rejecting + // the promise based on the error code. + archive.on("warning", function (err) { + if (err.code === "ENOENT") { + logger.log("warn", `Archiver issued a warning: ${err.code} - ${err.message}`); + } else { + reject(err); + } + }); + + // Set up an event listener for the 'error' event to reject the promise if any error occurs during archiving. + archive.on("error", function (err) { + reject(err); + }); + + // Pipe archive data to the file. + archive.pipe(output); + + // Add the directory to the archive, under the provided directory name. + archive.directory(directoryToZip, containerDirName); + + // Finalize the archive, indicating that no more files will be added and triggering the 'close' event once all + // data has been written. + archive.finalize(); + }); +} + +// Engage! +main(); diff --git a/TypeScript/2EditDatabase/package.json b/TypeScript/2EditDatabase/package.json index 18c5e98..a03160a 100644 --- a/TypeScript/2EditDatabase/package.json +++ b/TypeScript/2EditDatabase/package.json @@ -7,17 +7,20 @@ "akiVersion": "~3.7", "scripts": { "setup": "npm i", - "build": "node ./packageBuild.ts" + "build": "node ./build.mjs", + "buildinfo": "node ./build.mjs --verbose" }, "devDependencies": { "@types/node": "20.4.5", "@typescript-eslint/eslint-plugin": "6.2.0", "@typescript-eslint/parser": "6.2.0", - "bestzip": "2.2.1", + "archiver": "^6.0", "eslint": "8.46.0", - "fs-extra": "11.1.1", - "glob": "10.3.3", + "fs-extra": "^11.1", + "ignore": "^5.2", + "os": "^0.1", "tsyringe": "4.8.0", - "typescript": "5.1.6" + "typescript": "5.1.6", + "winston": "^3.9" } -} \ No newline at end of file +} diff --git a/TypeScript/2EditDatabase/packageBuild.ts b/TypeScript/2EditDatabase/packageBuild.ts deleted file mode 100644 index d76ddd8..0000000 --- a/TypeScript/2EditDatabase/packageBuild.ts +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env node - -// This is a simple script used to build a mod package. The script will copy necessary files to the build directory -// and compress the build directory into a zip file that can be easily shared. - -const fs = require("fs-extra"); -const glob = require("glob"); -const zip = require('bestzip'); -const path = require("path"); -const minimatch = require('minimatch'); - -// Load the package.json file to get some information about the package so we can name things appropriately. This is -// atypical, and you would never do this in a production environment, but this script is only used for development so -// it's fine in this case. Some of these values are stored in environment variables, but those differ between node -// versions; the 'author' value is not available after node v14. -const { author, name:packageName, version } = require("./package.json"); - -// Generate the name of the package, stripping out all non-alphanumeric characters in the 'author' and 'name'. -const modName = `${author.replace(/[^a-z0-9]/gi, "")}-${packageName.replace(/[^a-z0-9]/gi, "")}-${version}`; -console.log(`Generated package name: ${modName}`); - -// Delete the old build directory and compressed package file. -fs.rmSync(`${__dirname}/dist`, { force: true, recursive: true }); -console.log("Previous build files deleted."); - -// Generate a list of files that should not be copied over into the distribution directory. This is a blacklist to ensure -// we always copy over additional files and directories that authors may have added to their project. This may need to be -// expanded upon by the mod author to allow for node modules that are used within the mod; example commented out below. -const ignoreList = [ - "node_modules/", - // "node_modules/!(weighted|glob)", // Instead of excluding the entire node_modules directory, allow two node modules. - "src/**/*.js", - "types/", - ".git/", - ".gitea/", - ".eslintignore", - ".eslintrc.json", - ".gitignore", - ".DS_Store", - "packageBuild.ts", - "mod.code-workspace", - "package-lock.json", - "tsconfig.json" -]; -const exclude = glob.sync(`{${ignoreList.join(",")}}`, { realpath: true, dot: true }); - -fs.copySync(__dirname, path.normalize(`${__dirname}/../~${modName}`), {filter: (src) => -{ - const relativePath = path.relative(__dirname, src); - const shouldExclude = exclude.some((pattern) => minimatch(relativePath, pattern)); - console.log(`${relativePath} - Excluded: ${shouldExclude}`); - return !shouldExclude; -},}); - -fs.moveSync(path.normalize(`${__dirname}/../~${modName}`), path.normalize(`${__dirname}/${modName}`), { overwrite: true }); -fs.copySync(path.normalize(`${__dirname}/${modName}`), path.normalize(`${__dirname}/dist`)); -console.log("Build files copied."); - -// Compress the files for easy distribution. The compressed file is saved into the dist directory. When uncompressed we -// need to be sure that it includes a directory that the user can easily copy into their game mods directory. -zip({ - source: modName, - destination: `dist/${modName}.zip`, - cwd: __dirname -}).catch(function(err) -{ - console.error("A bestzip error has occurred: ", err.stack); -}).then(function() -{ - console.log(`Compressed mod package to: /dist/${modName}.zip`); - - // Now that we're done with the compression we can delete the temporary build directory. - fs.rmSync(`${__dirname}/${modName}`, { force: true, recursive: true }); - console.log("Build successful! your zip file has been created and is ready to be uploaded to hub.sp-tarkov.com/files/"); -}); \ No newline at end of file diff --git a/TypeScript/3GetSptConfigFile/.buildignore b/TypeScript/3GetSptConfigFile/.buildignore new file mode 100644 index 0000000..2cbde65 --- /dev/null +++ b/TypeScript/3GetSptConfigFile/.buildignore @@ -0,0 +1,20 @@ +/.buildignore +/.DS_Store +/.editorconfig +/.eslintignore +/.eslintrc.json +/.git +/.github +/.gitignore +/.gitlab +/.nvmrc +/.prettierrc +/.vscode +/build.mjs +/dist +/images +/mod.code-workspace +/node_modules +/package-lock.json +/tsconfig.json +/types diff --git a/TypeScript/3GetSptConfigFile/.eslintrc.json b/TypeScript/3GetSptConfigFile/.eslintrc.json index c505160..071a313 100644 --- a/TypeScript/3GetSptConfigFile/.eslintrc.json +++ b/TypeScript/3GetSptConfigFile/.eslintrc.json @@ -1,75 +1,98 @@ { - "root": true, - "parser": "@typescript-eslint/parser", - "plugins": [ - "@typescript-eslint" - ], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended" - ], - "rules": { - "@typescript-eslint/no-explicit-any": 0, - "@typescript-eslint/no-unused-vars": 1, - "@typescript-eslint/no-empty-interface": 0, - "@typescript-eslint/no-namespace": 0, - "@typescript-eslint/comma-dangle": 1, - "@typescript-eslint/func-call-spacing": 2, - "@typescript-eslint/quotes": 1, - "@typescript-eslint/brace-style": [ - "warn", - "allman" + "root": true, + "parser": "@typescript-eslint/parser", + "plugins": [ + "@typescript-eslint" ], - "@typescript-eslint/naming-convention": [ - "warn", - { - "selector": "default", - "format": ["camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "typeLike", - "format": ["PascalCase"] - }, - { - "selector": "objectLiteralProperty", - "format": ["PascalCase", "camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "typeProperty", - "format": ["PascalCase", "camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "enumMember", - "format": ["UPPER_CASE"] - } + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" ], - "@typescript-eslint/indent": [ - "warn", - 4 - ], - "@typescript-eslint/no-unused-expressions": [ - "warn", - { - "allowShortCircuit": false, - "allowTernary": false - } - ], - "@typescript-eslint/keyword-spacing": [ - "warn", - { - "before": true, - "after": true - } - ], - "@typescript-eslint/explicit-module-boundary-types": [ - "warn", - { - "allowArgumentsExplicitlyTypedAsAny": true - } + "rules": { + "@typescript-eslint/no-explicit-any": 0, + "@typescript-eslint/no-unused-vars": 1, + "@typescript-eslint/no-empty-interface": 0, + "@typescript-eslint/no-namespace": 0, + "@typescript-eslint/comma-dangle": 1, + "@typescript-eslint/func-call-spacing": 2, + "@typescript-eslint/quotes": 1, + "@typescript-eslint/brace-style": [ + "warn", + "allman" + ], + "@typescript-eslint/naming-convention": [ + "warn", + { + "selector": "default", + "format": [ + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "typeLike", + "format": [ + "PascalCase" + ] + }, + { + "selector": "objectLiteralProperty", + "format": [ + "PascalCase", + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "typeProperty", + "format": [ + "PascalCase", + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "enumMember", + "format": [ + "UPPER_CASE" + ] + } + ], + "@typescript-eslint/indent": [ + "warn", + 4 + ], + "@typescript-eslint/no-unused-expressions": [ + "warn", + { + "allowShortCircuit": false, + "allowTernary": false + } + ], + "@typescript-eslint/keyword-spacing": [ + "warn", + { + "before": true, + "after": true + } + ], + "@typescript-eslint/explicit-module-boundary-types": [ + "warn", + { + "allowArgumentsExplicitlyTypedAsAny": true + } + ] + }, + "overrides": [ + { + "files": [ + "*.mjs", + "*.ts" + ], + "env": { + "node": true + } + } ] - } } \ No newline at end of file diff --git a/TypeScript/3GetSptConfigFile/README.md b/TypeScript/3GetSptConfigFile/README.md index 434d063..8cba28e 100644 --- a/TypeScript/3GetSptConfigFile/README.md +++ b/TypeScript/3GetSptConfigFile/README.md @@ -1,64 +1,66 @@ +# Welcome to the SPT-AKI Modding Project -This project was created to automate most parts of building and setting up an environment. +This project is designed to streamline the initial setup process for building and creating mods in the SPT-AKI environment. Follow this guide to set up your environment efficiently. -## **NodeJS:** +## **Table of Contents** +- [NodeJS Setup](#nodejs-setup) +- [IDE Setup](#ide-setup) +- [Workspace Configuration](#workspace-configuration) +- [Environment Setup](#environment-setup) +- [Essential Concepts](#essential-concepts) +- [Coding Guidelines](#coding-guidelines) +- [Distribution Guidelines](#distribution-guidelines) -The first step would be to install nodejs on your pc, the version you NEED is **16.17.1** +## **NodeJS Setup** -That version is the one that has been used to test the mod templates and build scripts. +Before you begin, ensure to install NodeJS version `v16.17.1`, which has been tested thoroughly with our mod templates and build scripts. Download it from the [official NodeJS website](https://nodejs.org/). -It can be downloaded from here: https://nodejs.org/dist/v16.17.1/node-v16.17.1-x64.msi +After installation, it's advised to reboot your system. -A system reboot may be needed after install. +## **IDE Setup** -## **IDE:** +For this project, you can work with either [VSCodium](https://vscodium.com/) or [VSCode](https://code.visualstudio.com/). However, we strongly recommend using VSCode, as all development and testing have been carried out using this IDE, ensuring a smoother experience and compatibility with the project setups. Either way, we have a prepared a workspace file to assist you in setting up your environment. -The second step is having an IDE ready. We've setup a VSCodium workspace file to help with this. +## **Workspace Configuration** -You CAN use Visual Studio Code if you so desire, just keep in mind that our devs tests on the mod files was done using VSCode. +With NodeJS and your chosen IDE ready, initiate the `mod.code-workspace` file using your IDE: -You can get VSCode here: https://code.visualstudio.com/ +> File -> Open Workspace from File... -## **Workspace:** +Upon project loading, consider installing recommended plugins like the ESLint plugin. -Once you have NodeJS and VSCode/VSCodium ready, open the mod.code-workspace file with VSCode (File->Open Workspace from File...). +## **Environment Setup** -Once the project loads you may be recommended to install the ESLint plugin. This is HIGHLY recommended. +An automated task is available to configure your environment for Typescript utilization: -## **Environment Setup:** +> Terminal -> Run Task... -> Show All Tasks... -> npm: install -There is a task that will automatically setup your environment to use typescript. +Note: Preserve the `node_modules` folder as it contains necessary dependencies for Typescript and other functionalities. -To run it, you just need to go to: +## **Essential Concepts** -> Terminal->Run Task...->Show All Tasks...->npm: install +Prioritize understanding Dependency Injection and Inversion of Control, the architectural principles SPT-AKI adopts. Comprehensive guidelines will be available on the hub upon release. -After running this task, your environment will be ready to start coding. +Some resources to get you started: + - [A quick intro to Dependency Injection](https://www.freecodecamp.org/news/a-quick-intro-to-dependency-injection-what-it-is-and-when-to-use-it-7578c84fa88f/) + - [Understanding Inversion of Control (IoC) Principle](https://medium.com/@amitkma/understanding-inversion-of-control-ioc-principle-163b1dc97454) -DO NOT remove the node_modules folder, this is an auto generated directory that has the required dependencies to be able to use typescript and more. +## **Coding Guidelines** -## **IMPORTANT:** +Focus your mod development around the `mod.ts` file. In the `package.json` file, only alter these properties: `"name"`, `"version"`, `"license"`, `"author"`, and `"akiVersion"`. -Before starting to work on your mod, we suggest you read about Dependency Injection and Inversion of Control as this is the adopted architecture SPT-AKI has adopted. +New to Typescript? Find comprehensive documentation on the [official website](https://www.typescriptlang.org/docs/). -It will be difficult to understand some of the problems you may be having if you dont understand the basics of it. +## **Distribution Guidelines** -A guide explaining all the essentials will be available on the hub on release for you to read about. +Automated tasks are set up to bundle all necessary files for your mod to function in SPT-AKI: -## **Coding:** +> Terminal -> Run Task... -> Show All Tasks... -> npm: build -All your work should be centered around the mod.ts file as an entry point. -You can ONLY change the following properties from the package.json file: `"name"`, `"version"`, `"license"`: `"MIT"`, `"author"`, `"akiVersion"`. +The ZIP output, located in the `dist` directory, contains all required files. Ensure all files are included and modify the `.buildignore` file as needed. This ZIP file is your uploadable asset for the hub. -If you have never used typescript before, you can read about it here: https://www.typescriptlang.org/docs/ +## **Conclusion** -## **Distributing your mod:** +With this setup, you're ready to begin modding with SPT-AKI. If you run into any trouble be sure to check out the [modding documentation on the hub](https://hub.sp-tarkov.com/doc/lexicon/66-modding/). If you really get stuck feel free to join us in the [#mods-development](https://discord.com/channels/875684761291599922/875803116409323562) official Discord channel. -The project has been set up with an automatic task that will copy and zip ALL required files for your mod to work on SPT-AKI. -To run this task you just need to go to: - -> Terminal->Run Task...->Show All Tasks...->npm: build:zip - -The output will be a mod.zip file that will appear on the root of the project. - -Always verify that all files were included into the zip file. \ No newline at end of file +Build something awesome! diff --git a/TypeScript/3GetSptConfigFile/build.mjs b/TypeScript/3GetSptConfigFile/build.mjs new file mode 100644 index 0000000..d00117c --- /dev/null +++ b/TypeScript/3GetSptConfigFile/build.mjs @@ -0,0 +1,383 @@ +#!/usr/bin/env node + +/** + * Build Script + * + * This script automates the build process for server-side SPT mod projects, facilitating the creation of distributable + * mod packages. It performs a series of operations as outlined below: + * - Loads the .buildignore file, which is used to list files that should be ignored during the build process. + * - Loads the package.json to get project details so a descriptive name can be created for the mod package. + * - Creates a distribution directory and a temporary working directory. + * - Copies files to the temporary directory while respecting the .buildignore rules. + * - Creates a zip archive of the project files. + * - Moves the zip file to the root of the distribution directory. + * - Cleans up the temporary directory. + * + * It's typical that this script be customized to suit the needs of each project. For example, the script can be updated + * to perform additional operations, such as moving the mod package to a specific location or uploading it to a server. + * This script is intended to be a starting point for developers to build upon. + * + * Usage: + * - Run this script using npm: `npm run build` + * - Use `npm run buildinfo` for detailed logging. + * + * Note: + * - Ensure that all necessary Node.js modules are installed before running the script: `npm install` + * - The script reads configurations from the `package.json` and `.buildignore` files; ensure they are correctly set up. + * + * @author Refringe + * @version v1.0.0 + */ + +import fs from "fs-extra"; +import os from "os"; +import path from "path"; +import { fileURLToPath } from "url"; +import { dirname } from "path"; +import ignore from "ignore"; +import archiver from "archiver"; +import winston from "winston"; + +// Get the command line arguments to determine whether to use verbose logging. +const args = process.argv.slice(2); +const verbose = args.includes("--verbose") || args.includes("-v"); + +// Configure the Winston logger to use colours. +const logColors = { + error: "red", + warn: "yellow", + info: "grey", + success: "green", +}; +winston.addColors(logColors); + +// Create a logger instance to log build progress. Configure the logger levels to allow for different levels of logging +// based on the verbosity flag, and set the console transport to log messages of the appropriate level. +const logger = winston.createLogger({ + levels: { + error: 0, + warn: 1, + success: 2, + info: 3, + }, + format: winston.format.combine( + winston.format.colorize(), + winston.format.printf(info => { + return `${info.level}: ${info.message}`; + }) + ), + transports: [ + new winston.transports.Console({ + level: verbose ? "info" : "success", + }), + ], +}); + +/** + * The main function orchestrates the build process for creating a distributable mod package. It leverages a series of + * helper functions to perform various tasks such as loading configuration files, setting up directories, copying files + * according to `.buildignore` rules, and creating a ZIP archive of the project files. + * + * Utilizes the Winston logger to provide information on the build status at different stages of the process. + * + * @returns {void} + */ +async function main() { + // Get the current directory where the script is being executed + const currentDir = getCurrentDirectory(); + + // Defining at this scope because we need to use it in the finally block. + let projectDir; + + try { + // Load the .buildignore file to set up an ignore handler for the build process. + const buildIgnorePatterns = await loadBuildIgnoreFile(currentDir); + + // Load the package.json file to get project details. + const packageJson = await loadPackageJson(currentDir); + + // Create a descriptive name for the mod package. + const projectName = createProjectName(packageJson); + logger.log("success", `Project name created: ${projectName}`); + + // Remove the old distribution directory and create a fresh one. + const distDir = await removeOldDistDirectory(currentDir); + logger.log("info", "Distribution directory successfully cleaned."); + + // Create a temporary working directory to perform the build operations. + projectDir = await createTemporaryDirectoryWithProjectName(projectName); + logger.log("success", "Temporary working directory successfully created."); + logger.log("info", projectDir); + + // Copy files to the temporary directory while respecting the .buildignore rules. + logger.log("info", "Beginning copy operation using .buildignore file..."); + await copyFiles(currentDir, projectDir, buildIgnorePatterns); + logger.log("success", "Files successfully copied to temporary directory."); + + // Create a zip archive of the project files. + logger.log("info", "Beginning folder compression..."); + const zipFilePath = path.join(path.dirname(projectDir), `${projectName}.zip`); + await createZipFile(projectDir, zipFilePath, "user/mods/" + projectName); + logger.log("success", "Archive successfully created."); + logger.log("info", zipFilePath); + + // Move the zip file inside of the project directory, within the temporary working directory. + const zipFileInProjectDir = path.join(projectDir, `${projectName}.zip`); + await fs.move(zipFilePath, zipFileInProjectDir); + logger.log("success", "Archive successfully moved."); + logger.log("info", zipFileInProjectDir); + + // Move the temporary directory into the distribution directory. + await fs.move(projectDir, distDir); + logger.log("success", "Temporary directory successfully moved into project distribution directory."); + + // Log the success message. Write out the path to the mod package. + logger.log("success", "------------------------------------"); + logger.log("success", "Build script completed successfully!"); + logger.log("success", "Your mod package has been created in the 'dist' directory:"); + logger.log("success", `/${path.relative(process.cwd(), path.join(distDir, `${projectName}.zip`))}`); + logger.log("success", "------------------------------------"); + if (!verbose) { + logger.log("success", "To see a detailed build log, use `npm run buildinfo`."); + logger.log("success", "------------------------------------"); + } + } catch (err) { + // If any of the file operations fail, log the error. + logger.log("error", "An error occurred: " + err); + } finally { + // Clean up the temporary directory, even if the build fails. + if (projectDir) { + try { + await fs.promises.rm(projectDir, { force: true, recursive: true }); + logger.log("info", "Cleaned temporary directory."); + } catch (err) { + logger.log("error", "Failed to clean temporary directory: " + err); + } + } + } +} + +/** + * Retrieves the current working directory where the script is being executed. This directory is used as a reference + * point for various file operations throughout the build process, ensuring that paths are resolved correctly regardless + * of the location from which the script is invoked. + * + * @returns {string} The absolute path of the current working directory. + */ +function getCurrentDirectory() { + return dirname(fileURLToPath(import.meta.url)); +} + +/** + * Loads the `.buildignore` file and sets up an ignore handler using the `ignore` module. The `.buildignore` file + * contains a list of patterns describing files and directories that should be ignored during the build process. The + * ignore handler created by this method is used to filter files and directories when copying them to the temporary + * directory, ensuring that only necessary files are included in the final mod package. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to an ignore handler. + */ +async function loadBuildIgnoreFile(currentDir) { + const buildIgnorePath = path.join(currentDir, ".buildignore"); + + try { + // Attempt to read the contents of the .buildignore file asynchronously. + const fileContent = await fs.promises.readFile(buildIgnorePath, "utf-8"); + + // Return a new ignore instance and add the rules from the .buildignore file (split by newlines). + return ignore().add(fileContent.split("\n")); + } catch (err) { + logger.log("warn", "Failed to read .buildignore file. No files or directories will be ignored."); + + // Return an empty ignore instance, ensuring the build process can continue. + return ignore(); + } +} + +/** + * Loads the `package.json` file and returns its content as a JSON object. The `package.json` file contains important + * project details such as the name and version, which are used in later stages of the build process to create a + * descriptive name for the mod package. The method reads the file from the current working directory, ensuring that it + * accurately reflects the current state of the project. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to a JSON object containing the contents of the `package.json`. + */ +async function loadPackageJson(currentDir) { + const packageJsonPath = path.join(currentDir, "package.json"); + + // Read the contents of the package.json file asynchronously as a UTF-8 string. + const packageJsonContent = await fs.promises.readFile(packageJsonPath, "utf-8"); + + return JSON.parse(packageJsonContent); +} + +/** + * Constructs a descriptive name for the mod package using details from the `package.json` file. The name is created by + * concatenating the project name, version, and a timestamp, resulting in a unique and descriptive file name for each + * build. This name is used as the base name for the temporary working directory and the final ZIP archive, helping to + * identify different versions of the mod package easily. + * + * @param {Object} packageJson - A JSON object containing the contents of the `package.json` file. + * @returns {string} A string representing the constructed project name. + */ +function createProjectName(packageJson) { + // Remove any non-alphanumeric characters from the author and name. + const author = packageJson.author.replace(/\W/g, ""); + const name = packageJson.name.replace(/\W/g, ""); + const version = packageJson.version; + + // Ensure the name is lowercase, as per the package.json specification. + return `${author}-${name}-${version}`.toLowerCase(); +} + +/** + * Defines the location of the distribution directory where the final mod package will be stored and deletes any + * existing distribution directory to ensure a clean slate for the build process. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to the absolute path to the distribution directory. + */ +async function removeOldDistDirectory(projectDir) { + const distPath = path.join(projectDir, "dist"); + await fs.remove(distPath); + return distPath; +} + +/** + * Creates a temporary working directory using the project name. This directory serves as a staging area where project + * files are gathered before being archived into the final mod package. The method constructs a unique directory path + * by appending the project name to a base temporary directory path, ensuring that each build has its own isolated + * working space. This approach facilitates clean and organized build processes, avoiding potential conflicts with other + * builds. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @param {string} projectName - The constructed project name, used to create a unique path for the temporary directory. + * @returns {Promise} A promise that resolves to the absolute path of the newly created temporary directory. + */ +async function createTemporaryDirectoryWithProjectName(projectName) { + // Create a new directory in the system's temporary folder to hold the project files. + const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "spt-mod-build-")); + + // Create a subdirectory within the temporary directory using the project name for this specific build. + const projectDir = path.join(tempDir, projectName); + await fs.ensureDir(projectDir); + + return projectDir; +} + +/** + * Copies the project files to the temporary directory while respecting the rules defined in the `.buildignore` file. + * The method is recursive, iterating over all files and directories in the source directory and using the ignore + * handler to filter out files and directories that match the patterns defined in the `.buildignore` file. This ensures + * that only the necessary files are included in the final mod package, adhering to the specifications defined by the + * developer in the `.buildignore` file. + * + * The copy operations are delayed and executed in parallel to improve efficiency and reduce the build time. This is + * achieved by creating an array of copy promises and awaiting them all at the end of the function. + * + * @param {string} sourceDirectory - The absolute path of the current working directory. + * @param {string} destinationDirectory - The absolute path of the temporary directory where the files will be copied. + * @param {Ignore} ignoreHandler - The ignore handler created from the `.buildignore` file. + * @returns {Promise} A promise that resolves when all copy operations are completed successfully. + */ +async function copyFiles(srcDir, destDir, ignoreHandler) { + try { + // Read the contents of the source directory to get a list of entries (files and directories). + const entries = await fs.promises.readdir(srcDir, { withFileTypes: true }); + + // Initialize an array to hold the promises returned by recursive calls to copyFiles and copyFile operations. + const copyOperations = []; + + for (const entry of entries) { + // Define the source and destination paths for each entry. + const srcPath = path.join(srcDir, entry.name); + const destPath = path.join(destDir, entry.name); + + // Get the relative path of the source file to check against the ignore handler. + const relativePath = path.relative(process.cwd(), srcPath); + + // If the ignore handler dictates that this file should be ignored, skip to the next iteration. + if (ignoreHandler.ignores(relativePath)) { + logger.log("info", `Ignored: /${path.relative(process.cwd(), srcPath)}`); + continue; + } + + if (entry.isDirectory()) { + // If the entry is a directory, create the corresponding temporary directory and make a recursive call + // to copyFiles to handle copying the contents of the directory. + await fs.ensureDir(destPath); + copyOperations.push(copyFiles(srcPath, destPath, ignoreHandler)); + } else { + // If the entry is a file, add a copyFile operation to the copyOperations array and log the event when + // the operation is successful. + copyOperations.push( + fs.copy(srcPath, destPath).then(() => { + logger.log("info", `Copied: /${path.relative(process.cwd(), srcPath)}`); + }) + ); + } + } + + // Await all copy operations to ensure all files and directories are copied before exiting the function. + await Promise.all(copyOperations); + } catch (err) { + // Log an error message if any error occurs during the copy process. + logger.log("error", "Error copying files: " + err); + } +} + +/** + * Creates a ZIP archive of the project files located in the temporary directory. The method uses the `archiver` module + * to create a ZIP file, which includes all the files that have been copied to the temporary directory during the build + * process. The ZIP file is named using the project name, helping to identify the contents of the archive easily. + * + * @param {string} directoryPath - The absolute path of the temporary directory containing the project files. + * @param {string} projectName - The constructed project name, used to name the ZIP file. + * @returns {Promise} A promise that resolves to the absolute path of the created ZIP file. + */ +async function createZipFile(directoryToZip, zipFilePath, containerDirName) { + return new Promise((resolve, reject) => { + // Create a write stream to the specified ZIP file path. + const output = fs.createWriteStream(zipFilePath); + + // Create a new archiver instance with ZIP format and maximum compression level. + const archive = archiver("zip", { + zlib: { level: 9 }, + }); + + // Set up an event listener for the 'close' event to resolve the promise when the archiver has finalized. + output.on("close", function () { + logger.log("info", "Archiver has finalized. The output and the file descriptor have closed."); + resolve(); + }); + + // Set up an event listener for the 'warning' event to handle warnings appropriately, logging them or rejecting + // the promise based on the error code. + archive.on("warning", function (err) { + if (err.code === "ENOENT") { + logger.log("warn", `Archiver issued a warning: ${err.code} - ${err.message}`); + } else { + reject(err); + } + }); + + // Set up an event listener for the 'error' event to reject the promise if any error occurs during archiving. + archive.on("error", function (err) { + reject(err); + }); + + // Pipe archive data to the file. + archive.pipe(output); + + // Add the directory to the archive, under the provided directory name. + archive.directory(directoryToZip, containerDirName); + + // Finalize the archive, indicating that no more files will be added and triggering the 'close' event once all + // data has been written. + archive.finalize(); + }); +} + +// Engage! +main(); diff --git a/TypeScript/3GetSptConfigFile/package.json b/TypeScript/3GetSptConfigFile/package.json index eec7753..3e47235 100644 --- a/TypeScript/3GetSptConfigFile/package.json +++ b/TypeScript/3GetSptConfigFile/package.json @@ -7,17 +7,20 @@ "akiVersion": "~3.7", "scripts": { "setup": "npm i", - "build": "node ./packageBuild.ts" + "build": "node ./build.mjs", + "buildinfo": "node ./build.mjs --verbose" }, "devDependencies": { "@types/node": "20.4.5", "@typescript-eslint/eslint-plugin": "6.2.0", "@typescript-eslint/parser": "6.2.0", - "bestzip": "2.2.1", + "archiver": "^6.0", "eslint": "8.46.0", - "fs-extra": "11.1.1", - "glob": "10.3.3", + "fs-extra": "^11.1", + "ignore": "^5.2", + "os": "^0.1", "tsyringe": "4.8.0", - "typescript": "5.1.6" + "typescript": "5.1.6", + "winston": "^3.9" } -} \ No newline at end of file +} diff --git a/TypeScript/3GetSptConfigFile/packageBuild.ts b/TypeScript/3GetSptConfigFile/packageBuild.ts deleted file mode 100644 index d76ddd8..0000000 --- a/TypeScript/3GetSptConfigFile/packageBuild.ts +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env node - -// This is a simple script used to build a mod package. The script will copy necessary files to the build directory -// and compress the build directory into a zip file that can be easily shared. - -const fs = require("fs-extra"); -const glob = require("glob"); -const zip = require('bestzip'); -const path = require("path"); -const minimatch = require('minimatch'); - -// Load the package.json file to get some information about the package so we can name things appropriately. This is -// atypical, and you would never do this in a production environment, but this script is only used for development so -// it's fine in this case. Some of these values are stored in environment variables, but those differ between node -// versions; the 'author' value is not available after node v14. -const { author, name:packageName, version } = require("./package.json"); - -// Generate the name of the package, stripping out all non-alphanumeric characters in the 'author' and 'name'. -const modName = `${author.replace(/[^a-z0-9]/gi, "")}-${packageName.replace(/[^a-z0-9]/gi, "")}-${version}`; -console.log(`Generated package name: ${modName}`); - -// Delete the old build directory and compressed package file. -fs.rmSync(`${__dirname}/dist`, { force: true, recursive: true }); -console.log("Previous build files deleted."); - -// Generate a list of files that should not be copied over into the distribution directory. This is a blacklist to ensure -// we always copy over additional files and directories that authors may have added to their project. This may need to be -// expanded upon by the mod author to allow for node modules that are used within the mod; example commented out below. -const ignoreList = [ - "node_modules/", - // "node_modules/!(weighted|glob)", // Instead of excluding the entire node_modules directory, allow two node modules. - "src/**/*.js", - "types/", - ".git/", - ".gitea/", - ".eslintignore", - ".eslintrc.json", - ".gitignore", - ".DS_Store", - "packageBuild.ts", - "mod.code-workspace", - "package-lock.json", - "tsconfig.json" -]; -const exclude = glob.sync(`{${ignoreList.join(",")}}`, { realpath: true, dot: true }); - -fs.copySync(__dirname, path.normalize(`${__dirname}/../~${modName}`), {filter: (src) => -{ - const relativePath = path.relative(__dirname, src); - const shouldExclude = exclude.some((pattern) => minimatch(relativePath, pattern)); - console.log(`${relativePath} - Excluded: ${shouldExclude}`); - return !shouldExclude; -},}); - -fs.moveSync(path.normalize(`${__dirname}/../~${modName}`), path.normalize(`${__dirname}/${modName}`), { overwrite: true }); -fs.copySync(path.normalize(`${__dirname}/${modName}`), path.normalize(`${__dirname}/dist`)); -console.log("Build files copied."); - -// Compress the files for easy distribution. The compressed file is saved into the dist directory. When uncompressed we -// need to be sure that it includes a directory that the user can easily copy into their game mods directory. -zip({ - source: modName, - destination: `dist/${modName}.zip`, - cwd: __dirname -}).catch(function(err) -{ - console.error("A bestzip error has occurred: ", err.stack); -}).then(function() -{ - console.log(`Compressed mod package to: /dist/${modName}.zip`); - - // Now that we're done with the compression we can delete the temporary build directory. - fs.rmSync(`${__dirname}/${modName}`, { force: true, recursive: true }); - console.log("Build successful! your zip file has been created and is ready to be uploaded to hub.sp-tarkov.com/files/"); -}); \ No newline at end of file diff --git a/TypeScript/4UseACustomConfigFile/.buildignore b/TypeScript/4UseACustomConfigFile/.buildignore new file mode 100644 index 0000000..2cbde65 --- /dev/null +++ b/TypeScript/4UseACustomConfigFile/.buildignore @@ -0,0 +1,20 @@ +/.buildignore +/.DS_Store +/.editorconfig +/.eslintignore +/.eslintrc.json +/.git +/.github +/.gitignore +/.gitlab +/.nvmrc +/.prettierrc +/.vscode +/build.mjs +/dist +/images +/mod.code-workspace +/node_modules +/package-lock.json +/tsconfig.json +/types diff --git a/TypeScript/4UseACustomConfigFile/.eslintrc.json b/TypeScript/4UseACustomConfigFile/.eslintrc.json index c505160..071a313 100644 --- a/TypeScript/4UseACustomConfigFile/.eslintrc.json +++ b/TypeScript/4UseACustomConfigFile/.eslintrc.json @@ -1,75 +1,98 @@ { - "root": true, - "parser": "@typescript-eslint/parser", - "plugins": [ - "@typescript-eslint" - ], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended" - ], - "rules": { - "@typescript-eslint/no-explicit-any": 0, - "@typescript-eslint/no-unused-vars": 1, - "@typescript-eslint/no-empty-interface": 0, - "@typescript-eslint/no-namespace": 0, - "@typescript-eslint/comma-dangle": 1, - "@typescript-eslint/func-call-spacing": 2, - "@typescript-eslint/quotes": 1, - "@typescript-eslint/brace-style": [ - "warn", - "allman" + "root": true, + "parser": "@typescript-eslint/parser", + "plugins": [ + "@typescript-eslint" ], - "@typescript-eslint/naming-convention": [ - "warn", - { - "selector": "default", - "format": ["camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "typeLike", - "format": ["PascalCase"] - }, - { - "selector": "objectLiteralProperty", - "format": ["PascalCase", "camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "typeProperty", - "format": ["PascalCase", "camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "enumMember", - "format": ["UPPER_CASE"] - } + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" ], - "@typescript-eslint/indent": [ - "warn", - 4 - ], - "@typescript-eslint/no-unused-expressions": [ - "warn", - { - "allowShortCircuit": false, - "allowTernary": false - } - ], - "@typescript-eslint/keyword-spacing": [ - "warn", - { - "before": true, - "after": true - } - ], - "@typescript-eslint/explicit-module-boundary-types": [ - "warn", - { - "allowArgumentsExplicitlyTypedAsAny": true - } + "rules": { + "@typescript-eslint/no-explicit-any": 0, + "@typescript-eslint/no-unused-vars": 1, + "@typescript-eslint/no-empty-interface": 0, + "@typescript-eslint/no-namespace": 0, + "@typescript-eslint/comma-dangle": 1, + "@typescript-eslint/func-call-spacing": 2, + "@typescript-eslint/quotes": 1, + "@typescript-eslint/brace-style": [ + "warn", + "allman" + ], + "@typescript-eslint/naming-convention": [ + "warn", + { + "selector": "default", + "format": [ + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "typeLike", + "format": [ + "PascalCase" + ] + }, + { + "selector": "objectLiteralProperty", + "format": [ + "PascalCase", + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "typeProperty", + "format": [ + "PascalCase", + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "enumMember", + "format": [ + "UPPER_CASE" + ] + } + ], + "@typescript-eslint/indent": [ + "warn", + 4 + ], + "@typescript-eslint/no-unused-expressions": [ + "warn", + { + "allowShortCircuit": false, + "allowTernary": false + } + ], + "@typescript-eslint/keyword-spacing": [ + "warn", + { + "before": true, + "after": true + } + ], + "@typescript-eslint/explicit-module-boundary-types": [ + "warn", + { + "allowArgumentsExplicitlyTypedAsAny": true + } + ] + }, + "overrides": [ + { + "files": [ + "*.mjs", + "*.ts" + ], + "env": { + "node": true + } + } ] - } } \ No newline at end of file diff --git a/TypeScript/4UseACustomConfigFile/README.md b/TypeScript/4UseACustomConfigFile/README.md index 434d063..8cba28e 100644 --- a/TypeScript/4UseACustomConfigFile/README.md +++ b/TypeScript/4UseACustomConfigFile/README.md @@ -1,64 +1,66 @@ +# Welcome to the SPT-AKI Modding Project -This project was created to automate most parts of building and setting up an environment. +This project is designed to streamline the initial setup process for building and creating mods in the SPT-AKI environment. Follow this guide to set up your environment efficiently. -## **NodeJS:** +## **Table of Contents** +- [NodeJS Setup](#nodejs-setup) +- [IDE Setup](#ide-setup) +- [Workspace Configuration](#workspace-configuration) +- [Environment Setup](#environment-setup) +- [Essential Concepts](#essential-concepts) +- [Coding Guidelines](#coding-guidelines) +- [Distribution Guidelines](#distribution-guidelines) -The first step would be to install nodejs on your pc, the version you NEED is **16.17.1** +## **NodeJS Setup** -That version is the one that has been used to test the mod templates and build scripts. +Before you begin, ensure to install NodeJS version `v16.17.1`, which has been tested thoroughly with our mod templates and build scripts. Download it from the [official NodeJS website](https://nodejs.org/). -It can be downloaded from here: https://nodejs.org/dist/v16.17.1/node-v16.17.1-x64.msi +After installation, it's advised to reboot your system. -A system reboot may be needed after install. +## **IDE Setup** -## **IDE:** +For this project, you can work with either [VSCodium](https://vscodium.com/) or [VSCode](https://code.visualstudio.com/). However, we strongly recommend using VSCode, as all development and testing have been carried out using this IDE, ensuring a smoother experience and compatibility with the project setups. Either way, we have a prepared a workspace file to assist you in setting up your environment. -The second step is having an IDE ready. We've setup a VSCodium workspace file to help with this. +## **Workspace Configuration** -You CAN use Visual Studio Code if you so desire, just keep in mind that our devs tests on the mod files was done using VSCode. +With NodeJS and your chosen IDE ready, initiate the `mod.code-workspace` file using your IDE: -You can get VSCode here: https://code.visualstudio.com/ +> File -> Open Workspace from File... -## **Workspace:** +Upon project loading, consider installing recommended plugins like the ESLint plugin. -Once you have NodeJS and VSCode/VSCodium ready, open the mod.code-workspace file with VSCode (File->Open Workspace from File...). +## **Environment Setup** -Once the project loads you may be recommended to install the ESLint plugin. This is HIGHLY recommended. +An automated task is available to configure your environment for Typescript utilization: -## **Environment Setup:** +> Terminal -> Run Task... -> Show All Tasks... -> npm: install -There is a task that will automatically setup your environment to use typescript. +Note: Preserve the `node_modules` folder as it contains necessary dependencies for Typescript and other functionalities. -To run it, you just need to go to: +## **Essential Concepts** -> Terminal->Run Task...->Show All Tasks...->npm: install +Prioritize understanding Dependency Injection and Inversion of Control, the architectural principles SPT-AKI adopts. Comprehensive guidelines will be available on the hub upon release. -After running this task, your environment will be ready to start coding. +Some resources to get you started: + - [A quick intro to Dependency Injection](https://www.freecodecamp.org/news/a-quick-intro-to-dependency-injection-what-it-is-and-when-to-use-it-7578c84fa88f/) + - [Understanding Inversion of Control (IoC) Principle](https://medium.com/@amitkma/understanding-inversion-of-control-ioc-principle-163b1dc97454) -DO NOT remove the node_modules folder, this is an auto generated directory that has the required dependencies to be able to use typescript and more. +## **Coding Guidelines** -## **IMPORTANT:** +Focus your mod development around the `mod.ts` file. In the `package.json` file, only alter these properties: `"name"`, `"version"`, `"license"`, `"author"`, and `"akiVersion"`. -Before starting to work on your mod, we suggest you read about Dependency Injection and Inversion of Control as this is the adopted architecture SPT-AKI has adopted. +New to Typescript? Find comprehensive documentation on the [official website](https://www.typescriptlang.org/docs/). -It will be difficult to understand some of the problems you may be having if you dont understand the basics of it. +## **Distribution Guidelines** -A guide explaining all the essentials will be available on the hub on release for you to read about. +Automated tasks are set up to bundle all necessary files for your mod to function in SPT-AKI: -## **Coding:** +> Terminal -> Run Task... -> Show All Tasks... -> npm: build -All your work should be centered around the mod.ts file as an entry point. -You can ONLY change the following properties from the package.json file: `"name"`, `"version"`, `"license"`: `"MIT"`, `"author"`, `"akiVersion"`. +The ZIP output, located in the `dist` directory, contains all required files. Ensure all files are included and modify the `.buildignore` file as needed. This ZIP file is your uploadable asset for the hub. -If you have never used typescript before, you can read about it here: https://www.typescriptlang.org/docs/ +## **Conclusion** -## **Distributing your mod:** +With this setup, you're ready to begin modding with SPT-AKI. If you run into any trouble be sure to check out the [modding documentation on the hub](https://hub.sp-tarkov.com/doc/lexicon/66-modding/). If you really get stuck feel free to join us in the [#mods-development](https://discord.com/channels/875684761291599922/875803116409323562) official Discord channel. -The project has been set up with an automatic task that will copy and zip ALL required files for your mod to work on SPT-AKI. -To run this task you just need to go to: - -> Terminal->Run Task...->Show All Tasks...->npm: build:zip - -The output will be a mod.zip file that will appear on the root of the project. - -Always verify that all files were included into the zip file. \ No newline at end of file +Build something awesome! diff --git a/TypeScript/4UseACustomConfigFile/build.mjs b/TypeScript/4UseACustomConfigFile/build.mjs new file mode 100644 index 0000000..d00117c --- /dev/null +++ b/TypeScript/4UseACustomConfigFile/build.mjs @@ -0,0 +1,383 @@ +#!/usr/bin/env node + +/** + * Build Script + * + * This script automates the build process for server-side SPT mod projects, facilitating the creation of distributable + * mod packages. It performs a series of operations as outlined below: + * - Loads the .buildignore file, which is used to list files that should be ignored during the build process. + * - Loads the package.json to get project details so a descriptive name can be created for the mod package. + * - Creates a distribution directory and a temporary working directory. + * - Copies files to the temporary directory while respecting the .buildignore rules. + * - Creates a zip archive of the project files. + * - Moves the zip file to the root of the distribution directory. + * - Cleans up the temporary directory. + * + * It's typical that this script be customized to suit the needs of each project. For example, the script can be updated + * to perform additional operations, such as moving the mod package to a specific location or uploading it to a server. + * This script is intended to be a starting point for developers to build upon. + * + * Usage: + * - Run this script using npm: `npm run build` + * - Use `npm run buildinfo` for detailed logging. + * + * Note: + * - Ensure that all necessary Node.js modules are installed before running the script: `npm install` + * - The script reads configurations from the `package.json` and `.buildignore` files; ensure they are correctly set up. + * + * @author Refringe + * @version v1.0.0 + */ + +import fs from "fs-extra"; +import os from "os"; +import path from "path"; +import { fileURLToPath } from "url"; +import { dirname } from "path"; +import ignore from "ignore"; +import archiver from "archiver"; +import winston from "winston"; + +// Get the command line arguments to determine whether to use verbose logging. +const args = process.argv.slice(2); +const verbose = args.includes("--verbose") || args.includes("-v"); + +// Configure the Winston logger to use colours. +const logColors = { + error: "red", + warn: "yellow", + info: "grey", + success: "green", +}; +winston.addColors(logColors); + +// Create a logger instance to log build progress. Configure the logger levels to allow for different levels of logging +// based on the verbosity flag, and set the console transport to log messages of the appropriate level. +const logger = winston.createLogger({ + levels: { + error: 0, + warn: 1, + success: 2, + info: 3, + }, + format: winston.format.combine( + winston.format.colorize(), + winston.format.printf(info => { + return `${info.level}: ${info.message}`; + }) + ), + transports: [ + new winston.transports.Console({ + level: verbose ? "info" : "success", + }), + ], +}); + +/** + * The main function orchestrates the build process for creating a distributable mod package. It leverages a series of + * helper functions to perform various tasks such as loading configuration files, setting up directories, copying files + * according to `.buildignore` rules, and creating a ZIP archive of the project files. + * + * Utilizes the Winston logger to provide information on the build status at different stages of the process. + * + * @returns {void} + */ +async function main() { + // Get the current directory where the script is being executed + const currentDir = getCurrentDirectory(); + + // Defining at this scope because we need to use it in the finally block. + let projectDir; + + try { + // Load the .buildignore file to set up an ignore handler for the build process. + const buildIgnorePatterns = await loadBuildIgnoreFile(currentDir); + + // Load the package.json file to get project details. + const packageJson = await loadPackageJson(currentDir); + + // Create a descriptive name for the mod package. + const projectName = createProjectName(packageJson); + logger.log("success", `Project name created: ${projectName}`); + + // Remove the old distribution directory and create a fresh one. + const distDir = await removeOldDistDirectory(currentDir); + logger.log("info", "Distribution directory successfully cleaned."); + + // Create a temporary working directory to perform the build operations. + projectDir = await createTemporaryDirectoryWithProjectName(projectName); + logger.log("success", "Temporary working directory successfully created."); + logger.log("info", projectDir); + + // Copy files to the temporary directory while respecting the .buildignore rules. + logger.log("info", "Beginning copy operation using .buildignore file..."); + await copyFiles(currentDir, projectDir, buildIgnorePatterns); + logger.log("success", "Files successfully copied to temporary directory."); + + // Create a zip archive of the project files. + logger.log("info", "Beginning folder compression..."); + const zipFilePath = path.join(path.dirname(projectDir), `${projectName}.zip`); + await createZipFile(projectDir, zipFilePath, "user/mods/" + projectName); + logger.log("success", "Archive successfully created."); + logger.log("info", zipFilePath); + + // Move the zip file inside of the project directory, within the temporary working directory. + const zipFileInProjectDir = path.join(projectDir, `${projectName}.zip`); + await fs.move(zipFilePath, zipFileInProjectDir); + logger.log("success", "Archive successfully moved."); + logger.log("info", zipFileInProjectDir); + + // Move the temporary directory into the distribution directory. + await fs.move(projectDir, distDir); + logger.log("success", "Temporary directory successfully moved into project distribution directory."); + + // Log the success message. Write out the path to the mod package. + logger.log("success", "------------------------------------"); + logger.log("success", "Build script completed successfully!"); + logger.log("success", "Your mod package has been created in the 'dist' directory:"); + logger.log("success", `/${path.relative(process.cwd(), path.join(distDir, `${projectName}.zip`))}`); + logger.log("success", "------------------------------------"); + if (!verbose) { + logger.log("success", "To see a detailed build log, use `npm run buildinfo`."); + logger.log("success", "------------------------------------"); + } + } catch (err) { + // If any of the file operations fail, log the error. + logger.log("error", "An error occurred: " + err); + } finally { + // Clean up the temporary directory, even if the build fails. + if (projectDir) { + try { + await fs.promises.rm(projectDir, { force: true, recursive: true }); + logger.log("info", "Cleaned temporary directory."); + } catch (err) { + logger.log("error", "Failed to clean temporary directory: " + err); + } + } + } +} + +/** + * Retrieves the current working directory where the script is being executed. This directory is used as a reference + * point for various file operations throughout the build process, ensuring that paths are resolved correctly regardless + * of the location from which the script is invoked. + * + * @returns {string} The absolute path of the current working directory. + */ +function getCurrentDirectory() { + return dirname(fileURLToPath(import.meta.url)); +} + +/** + * Loads the `.buildignore` file and sets up an ignore handler using the `ignore` module. The `.buildignore` file + * contains a list of patterns describing files and directories that should be ignored during the build process. The + * ignore handler created by this method is used to filter files and directories when copying them to the temporary + * directory, ensuring that only necessary files are included in the final mod package. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to an ignore handler. + */ +async function loadBuildIgnoreFile(currentDir) { + const buildIgnorePath = path.join(currentDir, ".buildignore"); + + try { + // Attempt to read the contents of the .buildignore file asynchronously. + const fileContent = await fs.promises.readFile(buildIgnorePath, "utf-8"); + + // Return a new ignore instance and add the rules from the .buildignore file (split by newlines). + return ignore().add(fileContent.split("\n")); + } catch (err) { + logger.log("warn", "Failed to read .buildignore file. No files or directories will be ignored."); + + // Return an empty ignore instance, ensuring the build process can continue. + return ignore(); + } +} + +/** + * Loads the `package.json` file and returns its content as a JSON object. The `package.json` file contains important + * project details such as the name and version, which are used in later stages of the build process to create a + * descriptive name for the mod package. The method reads the file from the current working directory, ensuring that it + * accurately reflects the current state of the project. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to a JSON object containing the contents of the `package.json`. + */ +async function loadPackageJson(currentDir) { + const packageJsonPath = path.join(currentDir, "package.json"); + + // Read the contents of the package.json file asynchronously as a UTF-8 string. + const packageJsonContent = await fs.promises.readFile(packageJsonPath, "utf-8"); + + return JSON.parse(packageJsonContent); +} + +/** + * Constructs a descriptive name for the mod package using details from the `package.json` file. The name is created by + * concatenating the project name, version, and a timestamp, resulting in a unique and descriptive file name for each + * build. This name is used as the base name for the temporary working directory and the final ZIP archive, helping to + * identify different versions of the mod package easily. + * + * @param {Object} packageJson - A JSON object containing the contents of the `package.json` file. + * @returns {string} A string representing the constructed project name. + */ +function createProjectName(packageJson) { + // Remove any non-alphanumeric characters from the author and name. + const author = packageJson.author.replace(/\W/g, ""); + const name = packageJson.name.replace(/\W/g, ""); + const version = packageJson.version; + + // Ensure the name is lowercase, as per the package.json specification. + return `${author}-${name}-${version}`.toLowerCase(); +} + +/** + * Defines the location of the distribution directory where the final mod package will be stored and deletes any + * existing distribution directory to ensure a clean slate for the build process. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to the absolute path to the distribution directory. + */ +async function removeOldDistDirectory(projectDir) { + const distPath = path.join(projectDir, "dist"); + await fs.remove(distPath); + return distPath; +} + +/** + * Creates a temporary working directory using the project name. This directory serves as a staging area where project + * files are gathered before being archived into the final mod package. The method constructs a unique directory path + * by appending the project name to a base temporary directory path, ensuring that each build has its own isolated + * working space. This approach facilitates clean and organized build processes, avoiding potential conflicts with other + * builds. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @param {string} projectName - The constructed project name, used to create a unique path for the temporary directory. + * @returns {Promise} A promise that resolves to the absolute path of the newly created temporary directory. + */ +async function createTemporaryDirectoryWithProjectName(projectName) { + // Create a new directory in the system's temporary folder to hold the project files. + const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "spt-mod-build-")); + + // Create a subdirectory within the temporary directory using the project name for this specific build. + const projectDir = path.join(tempDir, projectName); + await fs.ensureDir(projectDir); + + return projectDir; +} + +/** + * Copies the project files to the temporary directory while respecting the rules defined in the `.buildignore` file. + * The method is recursive, iterating over all files and directories in the source directory and using the ignore + * handler to filter out files and directories that match the patterns defined in the `.buildignore` file. This ensures + * that only the necessary files are included in the final mod package, adhering to the specifications defined by the + * developer in the `.buildignore` file. + * + * The copy operations are delayed and executed in parallel to improve efficiency and reduce the build time. This is + * achieved by creating an array of copy promises and awaiting them all at the end of the function. + * + * @param {string} sourceDirectory - The absolute path of the current working directory. + * @param {string} destinationDirectory - The absolute path of the temporary directory where the files will be copied. + * @param {Ignore} ignoreHandler - The ignore handler created from the `.buildignore` file. + * @returns {Promise} A promise that resolves when all copy operations are completed successfully. + */ +async function copyFiles(srcDir, destDir, ignoreHandler) { + try { + // Read the contents of the source directory to get a list of entries (files and directories). + const entries = await fs.promises.readdir(srcDir, { withFileTypes: true }); + + // Initialize an array to hold the promises returned by recursive calls to copyFiles and copyFile operations. + const copyOperations = []; + + for (const entry of entries) { + // Define the source and destination paths for each entry. + const srcPath = path.join(srcDir, entry.name); + const destPath = path.join(destDir, entry.name); + + // Get the relative path of the source file to check against the ignore handler. + const relativePath = path.relative(process.cwd(), srcPath); + + // If the ignore handler dictates that this file should be ignored, skip to the next iteration. + if (ignoreHandler.ignores(relativePath)) { + logger.log("info", `Ignored: /${path.relative(process.cwd(), srcPath)}`); + continue; + } + + if (entry.isDirectory()) { + // If the entry is a directory, create the corresponding temporary directory and make a recursive call + // to copyFiles to handle copying the contents of the directory. + await fs.ensureDir(destPath); + copyOperations.push(copyFiles(srcPath, destPath, ignoreHandler)); + } else { + // If the entry is a file, add a copyFile operation to the copyOperations array and log the event when + // the operation is successful. + copyOperations.push( + fs.copy(srcPath, destPath).then(() => { + logger.log("info", `Copied: /${path.relative(process.cwd(), srcPath)}`); + }) + ); + } + } + + // Await all copy operations to ensure all files and directories are copied before exiting the function. + await Promise.all(copyOperations); + } catch (err) { + // Log an error message if any error occurs during the copy process. + logger.log("error", "Error copying files: " + err); + } +} + +/** + * Creates a ZIP archive of the project files located in the temporary directory. The method uses the `archiver` module + * to create a ZIP file, which includes all the files that have been copied to the temporary directory during the build + * process. The ZIP file is named using the project name, helping to identify the contents of the archive easily. + * + * @param {string} directoryPath - The absolute path of the temporary directory containing the project files. + * @param {string} projectName - The constructed project name, used to name the ZIP file. + * @returns {Promise} A promise that resolves to the absolute path of the created ZIP file. + */ +async function createZipFile(directoryToZip, zipFilePath, containerDirName) { + return new Promise((resolve, reject) => { + // Create a write stream to the specified ZIP file path. + const output = fs.createWriteStream(zipFilePath); + + // Create a new archiver instance with ZIP format and maximum compression level. + const archive = archiver("zip", { + zlib: { level: 9 }, + }); + + // Set up an event listener for the 'close' event to resolve the promise when the archiver has finalized. + output.on("close", function () { + logger.log("info", "Archiver has finalized. The output and the file descriptor have closed."); + resolve(); + }); + + // Set up an event listener for the 'warning' event to handle warnings appropriately, logging them or rejecting + // the promise based on the error code. + archive.on("warning", function (err) { + if (err.code === "ENOENT") { + logger.log("warn", `Archiver issued a warning: ${err.code} - ${err.message}`); + } else { + reject(err); + } + }); + + // Set up an event listener for the 'error' event to reject the promise if any error occurs during archiving. + archive.on("error", function (err) { + reject(err); + }); + + // Pipe archive data to the file. + archive.pipe(output); + + // Add the directory to the archive, under the provided directory name. + archive.directory(directoryToZip, containerDirName); + + // Finalize the archive, indicating that no more files will be added and triggering the 'close' event once all + // data has been written. + archive.finalize(); + }); +} + +// Engage! +main(); diff --git a/TypeScript/4UseACustomConfigFile/package.json b/TypeScript/4UseACustomConfigFile/package.json index 9bbe33a..f9d0be5 100644 --- a/TypeScript/4UseACustomConfigFile/package.json +++ b/TypeScript/4UseACustomConfigFile/package.json @@ -7,17 +7,20 @@ "akiVersion": "~3.7", "scripts": { "setup": "npm i", - "build": "node ./packageBuild.ts" + "build": "node ./build.mjs", + "buildinfo": "node ./build.mjs --verbose" }, "devDependencies": { "@types/node": "20.4.5", "@typescript-eslint/eslint-plugin": "6.2.0", "@typescript-eslint/parser": "6.2.0", - "bestzip": "2.2.1", + "archiver": "^6.0", "eslint": "8.46.0", - "fs-extra": "11.1.1", - "glob": "10.3.3", + "fs-extra": "^11.1", + "ignore": "^5.2", + "os": "^0.1", "tsyringe": "4.8.0", - "typescript": "5.1.6" + "typescript": "5.1.6", + "winston": "^3.9" } -} \ No newline at end of file +} diff --git a/TypeScript/4UseACustomConfigFile/packageBuild.ts b/TypeScript/4UseACustomConfigFile/packageBuild.ts deleted file mode 100644 index d76ddd8..0000000 --- a/TypeScript/4UseACustomConfigFile/packageBuild.ts +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env node - -// This is a simple script used to build a mod package. The script will copy necessary files to the build directory -// and compress the build directory into a zip file that can be easily shared. - -const fs = require("fs-extra"); -const glob = require("glob"); -const zip = require('bestzip'); -const path = require("path"); -const minimatch = require('minimatch'); - -// Load the package.json file to get some information about the package so we can name things appropriately. This is -// atypical, and you would never do this in a production environment, but this script is only used for development so -// it's fine in this case. Some of these values are stored in environment variables, but those differ between node -// versions; the 'author' value is not available after node v14. -const { author, name:packageName, version } = require("./package.json"); - -// Generate the name of the package, stripping out all non-alphanumeric characters in the 'author' and 'name'. -const modName = `${author.replace(/[^a-z0-9]/gi, "")}-${packageName.replace(/[^a-z0-9]/gi, "")}-${version}`; -console.log(`Generated package name: ${modName}`); - -// Delete the old build directory and compressed package file. -fs.rmSync(`${__dirname}/dist`, { force: true, recursive: true }); -console.log("Previous build files deleted."); - -// Generate a list of files that should not be copied over into the distribution directory. This is a blacklist to ensure -// we always copy over additional files and directories that authors may have added to their project. This may need to be -// expanded upon by the mod author to allow for node modules that are used within the mod; example commented out below. -const ignoreList = [ - "node_modules/", - // "node_modules/!(weighted|glob)", // Instead of excluding the entire node_modules directory, allow two node modules. - "src/**/*.js", - "types/", - ".git/", - ".gitea/", - ".eslintignore", - ".eslintrc.json", - ".gitignore", - ".DS_Store", - "packageBuild.ts", - "mod.code-workspace", - "package-lock.json", - "tsconfig.json" -]; -const exclude = glob.sync(`{${ignoreList.join(",")}}`, { realpath: true, dot: true }); - -fs.copySync(__dirname, path.normalize(`${__dirname}/../~${modName}`), {filter: (src) => -{ - const relativePath = path.relative(__dirname, src); - const shouldExclude = exclude.some((pattern) => minimatch(relativePath, pattern)); - console.log(`${relativePath} - Excluded: ${shouldExclude}`); - return !shouldExclude; -},}); - -fs.moveSync(path.normalize(`${__dirname}/../~${modName}`), path.normalize(`${__dirname}/${modName}`), { overwrite: true }); -fs.copySync(path.normalize(`${__dirname}/${modName}`), path.normalize(`${__dirname}/dist`)); -console.log("Build files copied."); - -// Compress the files for easy distribution. The compressed file is saved into the dist directory. When uncompressed we -// need to be sure that it includes a directory that the user can easily copy into their game mods directory. -zip({ - source: modName, - destination: `dist/${modName}.zip`, - cwd: __dirname -}).catch(function(err) -{ - console.error("A bestzip error has occurred: ", err.stack); -}).then(function() -{ - console.log(`Compressed mod package to: /dist/${modName}.zip`); - - // Now that we're done with the compression we can delete the temporary build directory. - fs.rmSync(`${__dirname}/${modName}`, { force: true, recursive: true }); - console.log("Build successful! your zip file has been created and is ready to be uploaded to hub.sp-tarkov.com/files/"); -}); \ No newline at end of file diff --git a/TypeScript/5ReplaceMethod/.buildignore b/TypeScript/5ReplaceMethod/.buildignore new file mode 100644 index 0000000..2cbde65 --- /dev/null +++ b/TypeScript/5ReplaceMethod/.buildignore @@ -0,0 +1,20 @@ +/.buildignore +/.DS_Store +/.editorconfig +/.eslintignore +/.eslintrc.json +/.git +/.github +/.gitignore +/.gitlab +/.nvmrc +/.prettierrc +/.vscode +/build.mjs +/dist +/images +/mod.code-workspace +/node_modules +/package-lock.json +/tsconfig.json +/types diff --git a/TypeScript/5ReplaceMethod/.eslintrc.json b/TypeScript/5ReplaceMethod/.eslintrc.json index c505160..071a313 100644 --- a/TypeScript/5ReplaceMethod/.eslintrc.json +++ b/TypeScript/5ReplaceMethod/.eslintrc.json @@ -1,75 +1,98 @@ { - "root": true, - "parser": "@typescript-eslint/parser", - "plugins": [ - "@typescript-eslint" - ], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended" - ], - "rules": { - "@typescript-eslint/no-explicit-any": 0, - "@typescript-eslint/no-unused-vars": 1, - "@typescript-eslint/no-empty-interface": 0, - "@typescript-eslint/no-namespace": 0, - "@typescript-eslint/comma-dangle": 1, - "@typescript-eslint/func-call-spacing": 2, - "@typescript-eslint/quotes": 1, - "@typescript-eslint/brace-style": [ - "warn", - "allman" + "root": true, + "parser": "@typescript-eslint/parser", + "plugins": [ + "@typescript-eslint" ], - "@typescript-eslint/naming-convention": [ - "warn", - { - "selector": "default", - "format": ["camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "typeLike", - "format": ["PascalCase"] - }, - { - "selector": "objectLiteralProperty", - "format": ["PascalCase", "camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "typeProperty", - "format": ["PascalCase", "camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "enumMember", - "format": ["UPPER_CASE"] - } + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" ], - "@typescript-eslint/indent": [ - "warn", - 4 - ], - "@typescript-eslint/no-unused-expressions": [ - "warn", - { - "allowShortCircuit": false, - "allowTernary": false - } - ], - "@typescript-eslint/keyword-spacing": [ - "warn", - { - "before": true, - "after": true - } - ], - "@typescript-eslint/explicit-module-boundary-types": [ - "warn", - { - "allowArgumentsExplicitlyTypedAsAny": true - } + "rules": { + "@typescript-eslint/no-explicit-any": 0, + "@typescript-eslint/no-unused-vars": 1, + "@typescript-eslint/no-empty-interface": 0, + "@typescript-eslint/no-namespace": 0, + "@typescript-eslint/comma-dangle": 1, + "@typescript-eslint/func-call-spacing": 2, + "@typescript-eslint/quotes": 1, + "@typescript-eslint/brace-style": [ + "warn", + "allman" + ], + "@typescript-eslint/naming-convention": [ + "warn", + { + "selector": "default", + "format": [ + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "typeLike", + "format": [ + "PascalCase" + ] + }, + { + "selector": "objectLiteralProperty", + "format": [ + "PascalCase", + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "typeProperty", + "format": [ + "PascalCase", + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "enumMember", + "format": [ + "UPPER_CASE" + ] + } + ], + "@typescript-eslint/indent": [ + "warn", + 4 + ], + "@typescript-eslint/no-unused-expressions": [ + "warn", + { + "allowShortCircuit": false, + "allowTernary": false + } + ], + "@typescript-eslint/keyword-spacing": [ + "warn", + { + "before": true, + "after": true + } + ], + "@typescript-eslint/explicit-module-boundary-types": [ + "warn", + { + "allowArgumentsExplicitlyTypedAsAny": true + } + ] + }, + "overrides": [ + { + "files": [ + "*.mjs", + "*.ts" + ], + "env": { + "node": true + } + } ] - } } \ No newline at end of file diff --git a/TypeScript/5ReplaceMethod/README.md b/TypeScript/5ReplaceMethod/README.md index 434d063..8cba28e 100644 --- a/TypeScript/5ReplaceMethod/README.md +++ b/TypeScript/5ReplaceMethod/README.md @@ -1,64 +1,66 @@ +# Welcome to the SPT-AKI Modding Project -This project was created to automate most parts of building and setting up an environment. +This project is designed to streamline the initial setup process for building and creating mods in the SPT-AKI environment. Follow this guide to set up your environment efficiently. -## **NodeJS:** +## **Table of Contents** +- [NodeJS Setup](#nodejs-setup) +- [IDE Setup](#ide-setup) +- [Workspace Configuration](#workspace-configuration) +- [Environment Setup](#environment-setup) +- [Essential Concepts](#essential-concepts) +- [Coding Guidelines](#coding-guidelines) +- [Distribution Guidelines](#distribution-guidelines) -The first step would be to install nodejs on your pc, the version you NEED is **16.17.1** +## **NodeJS Setup** -That version is the one that has been used to test the mod templates and build scripts. +Before you begin, ensure to install NodeJS version `v16.17.1`, which has been tested thoroughly with our mod templates and build scripts. Download it from the [official NodeJS website](https://nodejs.org/). -It can be downloaded from here: https://nodejs.org/dist/v16.17.1/node-v16.17.1-x64.msi +After installation, it's advised to reboot your system. -A system reboot may be needed after install. +## **IDE Setup** -## **IDE:** +For this project, you can work with either [VSCodium](https://vscodium.com/) or [VSCode](https://code.visualstudio.com/). However, we strongly recommend using VSCode, as all development and testing have been carried out using this IDE, ensuring a smoother experience and compatibility with the project setups. Either way, we have a prepared a workspace file to assist you in setting up your environment. -The second step is having an IDE ready. We've setup a VSCodium workspace file to help with this. +## **Workspace Configuration** -You CAN use Visual Studio Code if you so desire, just keep in mind that our devs tests on the mod files was done using VSCode. +With NodeJS and your chosen IDE ready, initiate the `mod.code-workspace` file using your IDE: -You can get VSCode here: https://code.visualstudio.com/ +> File -> Open Workspace from File... -## **Workspace:** +Upon project loading, consider installing recommended plugins like the ESLint plugin. -Once you have NodeJS and VSCode/VSCodium ready, open the mod.code-workspace file with VSCode (File->Open Workspace from File...). +## **Environment Setup** -Once the project loads you may be recommended to install the ESLint plugin. This is HIGHLY recommended. +An automated task is available to configure your environment for Typescript utilization: -## **Environment Setup:** +> Terminal -> Run Task... -> Show All Tasks... -> npm: install -There is a task that will automatically setup your environment to use typescript. +Note: Preserve the `node_modules` folder as it contains necessary dependencies for Typescript and other functionalities. -To run it, you just need to go to: +## **Essential Concepts** -> Terminal->Run Task...->Show All Tasks...->npm: install +Prioritize understanding Dependency Injection and Inversion of Control, the architectural principles SPT-AKI adopts. Comprehensive guidelines will be available on the hub upon release. -After running this task, your environment will be ready to start coding. +Some resources to get you started: + - [A quick intro to Dependency Injection](https://www.freecodecamp.org/news/a-quick-intro-to-dependency-injection-what-it-is-and-when-to-use-it-7578c84fa88f/) + - [Understanding Inversion of Control (IoC) Principle](https://medium.com/@amitkma/understanding-inversion-of-control-ioc-principle-163b1dc97454) -DO NOT remove the node_modules folder, this is an auto generated directory that has the required dependencies to be able to use typescript and more. +## **Coding Guidelines** -## **IMPORTANT:** +Focus your mod development around the `mod.ts` file. In the `package.json` file, only alter these properties: `"name"`, `"version"`, `"license"`, `"author"`, and `"akiVersion"`. -Before starting to work on your mod, we suggest you read about Dependency Injection and Inversion of Control as this is the adopted architecture SPT-AKI has adopted. +New to Typescript? Find comprehensive documentation on the [official website](https://www.typescriptlang.org/docs/). -It will be difficult to understand some of the problems you may be having if you dont understand the basics of it. +## **Distribution Guidelines** -A guide explaining all the essentials will be available on the hub on release for you to read about. +Automated tasks are set up to bundle all necessary files for your mod to function in SPT-AKI: -## **Coding:** +> Terminal -> Run Task... -> Show All Tasks... -> npm: build -All your work should be centered around the mod.ts file as an entry point. -You can ONLY change the following properties from the package.json file: `"name"`, `"version"`, `"license"`: `"MIT"`, `"author"`, `"akiVersion"`. +The ZIP output, located in the `dist` directory, contains all required files. Ensure all files are included and modify the `.buildignore` file as needed. This ZIP file is your uploadable asset for the hub. -If you have never used typescript before, you can read about it here: https://www.typescriptlang.org/docs/ +## **Conclusion** -## **Distributing your mod:** +With this setup, you're ready to begin modding with SPT-AKI. If you run into any trouble be sure to check out the [modding documentation on the hub](https://hub.sp-tarkov.com/doc/lexicon/66-modding/). If you really get stuck feel free to join us in the [#mods-development](https://discord.com/channels/875684761291599922/875803116409323562) official Discord channel. -The project has been set up with an automatic task that will copy and zip ALL required files for your mod to work on SPT-AKI. -To run this task you just need to go to: - -> Terminal->Run Task...->Show All Tasks...->npm: build:zip - -The output will be a mod.zip file that will appear on the root of the project. - -Always verify that all files were included into the zip file. \ No newline at end of file +Build something awesome! diff --git a/TypeScript/5ReplaceMethod/build.mjs b/TypeScript/5ReplaceMethod/build.mjs new file mode 100644 index 0000000..d00117c --- /dev/null +++ b/TypeScript/5ReplaceMethod/build.mjs @@ -0,0 +1,383 @@ +#!/usr/bin/env node + +/** + * Build Script + * + * This script automates the build process for server-side SPT mod projects, facilitating the creation of distributable + * mod packages. It performs a series of operations as outlined below: + * - Loads the .buildignore file, which is used to list files that should be ignored during the build process. + * - Loads the package.json to get project details so a descriptive name can be created for the mod package. + * - Creates a distribution directory and a temporary working directory. + * - Copies files to the temporary directory while respecting the .buildignore rules. + * - Creates a zip archive of the project files. + * - Moves the zip file to the root of the distribution directory. + * - Cleans up the temporary directory. + * + * It's typical that this script be customized to suit the needs of each project. For example, the script can be updated + * to perform additional operations, such as moving the mod package to a specific location or uploading it to a server. + * This script is intended to be a starting point for developers to build upon. + * + * Usage: + * - Run this script using npm: `npm run build` + * - Use `npm run buildinfo` for detailed logging. + * + * Note: + * - Ensure that all necessary Node.js modules are installed before running the script: `npm install` + * - The script reads configurations from the `package.json` and `.buildignore` files; ensure they are correctly set up. + * + * @author Refringe + * @version v1.0.0 + */ + +import fs from "fs-extra"; +import os from "os"; +import path from "path"; +import { fileURLToPath } from "url"; +import { dirname } from "path"; +import ignore from "ignore"; +import archiver from "archiver"; +import winston from "winston"; + +// Get the command line arguments to determine whether to use verbose logging. +const args = process.argv.slice(2); +const verbose = args.includes("--verbose") || args.includes("-v"); + +// Configure the Winston logger to use colours. +const logColors = { + error: "red", + warn: "yellow", + info: "grey", + success: "green", +}; +winston.addColors(logColors); + +// Create a logger instance to log build progress. Configure the logger levels to allow for different levels of logging +// based on the verbosity flag, and set the console transport to log messages of the appropriate level. +const logger = winston.createLogger({ + levels: { + error: 0, + warn: 1, + success: 2, + info: 3, + }, + format: winston.format.combine( + winston.format.colorize(), + winston.format.printf(info => { + return `${info.level}: ${info.message}`; + }) + ), + transports: [ + new winston.transports.Console({ + level: verbose ? "info" : "success", + }), + ], +}); + +/** + * The main function orchestrates the build process for creating a distributable mod package. It leverages a series of + * helper functions to perform various tasks such as loading configuration files, setting up directories, copying files + * according to `.buildignore` rules, and creating a ZIP archive of the project files. + * + * Utilizes the Winston logger to provide information on the build status at different stages of the process. + * + * @returns {void} + */ +async function main() { + // Get the current directory where the script is being executed + const currentDir = getCurrentDirectory(); + + // Defining at this scope because we need to use it in the finally block. + let projectDir; + + try { + // Load the .buildignore file to set up an ignore handler for the build process. + const buildIgnorePatterns = await loadBuildIgnoreFile(currentDir); + + // Load the package.json file to get project details. + const packageJson = await loadPackageJson(currentDir); + + // Create a descriptive name for the mod package. + const projectName = createProjectName(packageJson); + logger.log("success", `Project name created: ${projectName}`); + + // Remove the old distribution directory and create a fresh one. + const distDir = await removeOldDistDirectory(currentDir); + logger.log("info", "Distribution directory successfully cleaned."); + + // Create a temporary working directory to perform the build operations. + projectDir = await createTemporaryDirectoryWithProjectName(projectName); + logger.log("success", "Temporary working directory successfully created."); + logger.log("info", projectDir); + + // Copy files to the temporary directory while respecting the .buildignore rules. + logger.log("info", "Beginning copy operation using .buildignore file..."); + await copyFiles(currentDir, projectDir, buildIgnorePatterns); + logger.log("success", "Files successfully copied to temporary directory."); + + // Create a zip archive of the project files. + logger.log("info", "Beginning folder compression..."); + const zipFilePath = path.join(path.dirname(projectDir), `${projectName}.zip`); + await createZipFile(projectDir, zipFilePath, "user/mods/" + projectName); + logger.log("success", "Archive successfully created."); + logger.log("info", zipFilePath); + + // Move the zip file inside of the project directory, within the temporary working directory. + const zipFileInProjectDir = path.join(projectDir, `${projectName}.zip`); + await fs.move(zipFilePath, zipFileInProjectDir); + logger.log("success", "Archive successfully moved."); + logger.log("info", zipFileInProjectDir); + + // Move the temporary directory into the distribution directory. + await fs.move(projectDir, distDir); + logger.log("success", "Temporary directory successfully moved into project distribution directory."); + + // Log the success message. Write out the path to the mod package. + logger.log("success", "------------------------------------"); + logger.log("success", "Build script completed successfully!"); + logger.log("success", "Your mod package has been created in the 'dist' directory:"); + logger.log("success", `/${path.relative(process.cwd(), path.join(distDir, `${projectName}.zip`))}`); + logger.log("success", "------------------------------------"); + if (!verbose) { + logger.log("success", "To see a detailed build log, use `npm run buildinfo`."); + logger.log("success", "------------------------------------"); + } + } catch (err) { + // If any of the file operations fail, log the error. + logger.log("error", "An error occurred: " + err); + } finally { + // Clean up the temporary directory, even if the build fails. + if (projectDir) { + try { + await fs.promises.rm(projectDir, { force: true, recursive: true }); + logger.log("info", "Cleaned temporary directory."); + } catch (err) { + logger.log("error", "Failed to clean temporary directory: " + err); + } + } + } +} + +/** + * Retrieves the current working directory where the script is being executed. This directory is used as a reference + * point for various file operations throughout the build process, ensuring that paths are resolved correctly regardless + * of the location from which the script is invoked. + * + * @returns {string} The absolute path of the current working directory. + */ +function getCurrentDirectory() { + return dirname(fileURLToPath(import.meta.url)); +} + +/** + * Loads the `.buildignore` file and sets up an ignore handler using the `ignore` module. The `.buildignore` file + * contains a list of patterns describing files and directories that should be ignored during the build process. The + * ignore handler created by this method is used to filter files and directories when copying them to the temporary + * directory, ensuring that only necessary files are included in the final mod package. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to an ignore handler. + */ +async function loadBuildIgnoreFile(currentDir) { + const buildIgnorePath = path.join(currentDir, ".buildignore"); + + try { + // Attempt to read the contents of the .buildignore file asynchronously. + const fileContent = await fs.promises.readFile(buildIgnorePath, "utf-8"); + + // Return a new ignore instance and add the rules from the .buildignore file (split by newlines). + return ignore().add(fileContent.split("\n")); + } catch (err) { + logger.log("warn", "Failed to read .buildignore file. No files or directories will be ignored."); + + // Return an empty ignore instance, ensuring the build process can continue. + return ignore(); + } +} + +/** + * Loads the `package.json` file and returns its content as a JSON object. The `package.json` file contains important + * project details such as the name and version, which are used in later stages of the build process to create a + * descriptive name for the mod package. The method reads the file from the current working directory, ensuring that it + * accurately reflects the current state of the project. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to a JSON object containing the contents of the `package.json`. + */ +async function loadPackageJson(currentDir) { + const packageJsonPath = path.join(currentDir, "package.json"); + + // Read the contents of the package.json file asynchronously as a UTF-8 string. + const packageJsonContent = await fs.promises.readFile(packageJsonPath, "utf-8"); + + return JSON.parse(packageJsonContent); +} + +/** + * Constructs a descriptive name for the mod package using details from the `package.json` file. The name is created by + * concatenating the project name, version, and a timestamp, resulting in a unique and descriptive file name for each + * build. This name is used as the base name for the temporary working directory and the final ZIP archive, helping to + * identify different versions of the mod package easily. + * + * @param {Object} packageJson - A JSON object containing the contents of the `package.json` file. + * @returns {string} A string representing the constructed project name. + */ +function createProjectName(packageJson) { + // Remove any non-alphanumeric characters from the author and name. + const author = packageJson.author.replace(/\W/g, ""); + const name = packageJson.name.replace(/\W/g, ""); + const version = packageJson.version; + + // Ensure the name is lowercase, as per the package.json specification. + return `${author}-${name}-${version}`.toLowerCase(); +} + +/** + * Defines the location of the distribution directory where the final mod package will be stored and deletes any + * existing distribution directory to ensure a clean slate for the build process. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to the absolute path to the distribution directory. + */ +async function removeOldDistDirectory(projectDir) { + const distPath = path.join(projectDir, "dist"); + await fs.remove(distPath); + return distPath; +} + +/** + * Creates a temporary working directory using the project name. This directory serves as a staging area where project + * files are gathered before being archived into the final mod package. The method constructs a unique directory path + * by appending the project name to a base temporary directory path, ensuring that each build has its own isolated + * working space. This approach facilitates clean and organized build processes, avoiding potential conflicts with other + * builds. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @param {string} projectName - The constructed project name, used to create a unique path for the temporary directory. + * @returns {Promise} A promise that resolves to the absolute path of the newly created temporary directory. + */ +async function createTemporaryDirectoryWithProjectName(projectName) { + // Create a new directory in the system's temporary folder to hold the project files. + const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "spt-mod-build-")); + + // Create a subdirectory within the temporary directory using the project name for this specific build. + const projectDir = path.join(tempDir, projectName); + await fs.ensureDir(projectDir); + + return projectDir; +} + +/** + * Copies the project files to the temporary directory while respecting the rules defined in the `.buildignore` file. + * The method is recursive, iterating over all files and directories in the source directory and using the ignore + * handler to filter out files and directories that match the patterns defined in the `.buildignore` file. This ensures + * that only the necessary files are included in the final mod package, adhering to the specifications defined by the + * developer in the `.buildignore` file. + * + * The copy operations are delayed and executed in parallel to improve efficiency and reduce the build time. This is + * achieved by creating an array of copy promises and awaiting them all at the end of the function. + * + * @param {string} sourceDirectory - The absolute path of the current working directory. + * @param {string} destinationDirectory - The absolute path of the temporary directory where the files will be copied. + * @param {Ignore} ignoreHandler - The ignore handler created from the `.buildignore` file. + * @returns {Promise} A promise that resolves when all copy operations are completed successfully. + */ +async function copyFiles(srcDir, destDir, ignoreHandler) { + try { + // Read the contents of the source directory to get a list of entries (files and directories). + const entries = await fs.promises.readdir(srcDir, { withFileTypes: true }); + + // Initialize an array to hold the promises returned by recursive calls to copyFiles and copyFile operations. + const copyOperations = []; + + for (const entry of entries) { + // Define the source and destination paths for each entry. + const srcPath = path.join(srcDir, entry.name); + const destPath = path.join(destDir, entry.name); + + // Get the relative path of the source file to check against the ignore handler. + const relativePath = path.relative(process.cwd(), srcPath); + + // If the ignore handler dictates that this file should be ignored, skip to the next iteration. + if (ignoreHandler.ignores(relativePath)) { + logger.log("info", `Ignored: /${path.relative(process.cwd(), srcPath)}`); + continue; + } + + if (entry.isDirectory()) { + // If the entry is a directory, create the corresponding temporary directory and make a recursive call + // to copyFiles to handle copying the contents of the directory. + await fs.ensureDir(destPath); + copyOperations.push(copyFiles(srcPath, destPath, ignoreHandler)); + } else { + // If the entry is a file, add a copyFile operation to the copyOperations array and log the event when + // the operation is successful. + copyOperations.push( + fs.copy(srcPath, destPath).then(() => { + logger.log("info", `Copied: /${path.relative(process.cwd(), srcPath)}`); + }) + ); + } + } + + // Await all copy operations to ensure all files and directories are copied before exiting the function. + await Promise.all(copyOperations); + } catch (err) { + // Log an error message if any error occurs during the copy process. + logger.log("error", "Error copying files: " + err); + } +} + +/** + * Creates a ZIP archive of the project files located in the temporary directory. The method uses the `archiver` module + * to create a ZIP file, which includes all the files that have been copied to the temporary directory during the build + * process. The ZIP file is named using the project name, helping to identify the contents of the archive easily. + * + * @param {string} directoryPath - The absolute path of the temporary directory containing the project files. + * @param {string} projectName - The constructed project name, used to name the ZIP file. + * @returns {Promise} A promise that resolves to the absolute path of the created ZIP file. + */ +async function createZipFile(directoryToZip, zipFilePath, containerDirName) { + return new Promise((resolve, reject) => { + // Create a write stream to the specified ZIP file path. + const output = fs.createWriteStream(zipFilePath); + + // Create a new archiver instance with ZIP format and maximum compression level. + const archive = archiver("zip", { + zlib: { level: 9 }, + }); + + // Set up an event listener for the 'close' event to resolve the promise when the archiver has finalized. + output.on("close", function () { + logger.log("info", "Archiver has finalized. The output and the file descriptor have closed."); + resolve(); + }); + + // Set up an event listener for the 'warning' event to handle warnings appropriately, logging them or rejecting + // the promise based on the error code. + archive.on("warning", function (err) { + if (err.code === "ENOENT") { + logger.log("warn", `Archiver issued a warning: ${err.code} - ${err.message}`); + } else { + reject(err); + } + }); + + // Set up an event listener for the 'error' event to reject the promise if any error occurs during archiving. + archive.on("error", function (err) { + reject(err); + }); + + // Pipe archive data to the file. + archive.pipe(output); + + // Add the directory to the archive, under the provided directory name. + archive.directory(directoryToZip, containerDirName); + + // Finalize the archive, indicating that no more files will be added and triggering the 'close' event once all + // data has been written. + archive.finalize(); + }); +} + +// Engage! +main(); diff --git a/TypeScript/5ReplaceMethod/package.json b/TypeScript/5ReplaceMethod/package.json index b241352..8f6dffc 100644 --- a/TypeScript/5ReplaceMethod/package.json +++ b/TypeScript/5ReplaceMethod/package.json @@ -7,17 +7,20 @@ "akiVersion": "~3.7", "scripts": { "setup": "npm i", - "build": "node ./packageBuild.ts" + "build": "node ./build.mjs", + "buildinfo": "node ./build.mjs --verbose" }, "devDependencies": { "@types/node": "20.4.5", "@typescript-eslint/eslint-plugin": "6.2.0", "@typescript-eslint/parser": "6.2.0", - "bestzip": "2.2.1", + "archiver": "^6.0", "eslint": "8.46.0", - "fs-extra": "11.1.1", - "glob": "10.3.3", + "fs-extra": "^11.1", + "ignore": "^5.2", + "os": "^0.1", "tsyringe": "4.8.0", - "typescript": "5.1.6" + "typescript": "5.1.6", + "winston": "^3.9" } -} \ No newline at end of file +} diff --git a/TypeScript/5ReplaceMethod/packageBuild.ts b/TypeScript/5ReplaceMethod/packageBuild.ts deleted file mode 100644 index d76ddd8..0000000 --- a/TypeScript/5ReplaceMethod/packageBuild.ts +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env node - -// This is a simple script used to build a mod package. The script will copy necessary files to the build directory -// and compress the build directory into a zip file that can be easily shared. - -const fs = require("fs-extra"); -const glob = require("glob"); -const zip = require('bestzip'); -const path = require("path"); -const minimatch = require('minimatch'); - -// Load the package.json file to get some information about the package so we can name things appropriately. This is -// atypical, and you would never do this in a production environment, but this script is only used for development so -// it's fine in this case. Some of these values are stored in environment variables, but those differ between node -// versions; the 'author' value is not available after node v14. -const { author, name:packageName, version } = require("./package.json"); - -// Generate the name of the package, stripping out all non-alphanumeric characters in the 'author' and 'name'. -const modName = `${author.replace(/[^a-z0-9]/gi, "")}-${packageName.replace(/[^a-z0-9]/gi, "")}-${version}`; -console.log(`Generated package name: ${modName}`); - -// Delete the old build directory and compressed package file. -fs.rmSync(`${__dirname}/dist`, { force: true, recursive: true }); -console.log("Previous build files deleted."); - -// Generate a list of files that should not be copied over into the distribution directory. This is a blacklist to ensure -// we always copy over additional files and directories that authors may have added to their project. This may need to be -// expanded upon by the mod author to allow for node modules that are used within the mod; example commented out below. -const ignoreList = [ - "node_modules/", - // "node_modules/!(weighted|glob)", // Instead of excluding the entire node_modules directory, allow two node modules. - "src/**/*.js", - "types/", - ".git/", - ".gitea/", - ".eslintignore", - ".eslintrc.json", - ".gitignore", - ".DS_Store", - "packageBuild.ts", - "mod.code-workspace", - "package-lock.json", - "tsconfig.json" -]; -const exclude = glob.sync(`{${ignoreList.join(",")}}`, { realpath: true, dot: true }); - -fs.copySync(__dirname, path.normalize(`${__dirname}/../~${modName}`), {filter: (src) => -{ - const relativePath = path.relative(__dirname, src); - const shouldExclude = exclude.some((pattern) => minimatch(relativePath, pattern)); - console.log(`${relativePath} - Excluded: ${shouldExclude}`); - return !shouldExclude; -},}); - -fs.moveSync(path.normalize(`${__dirname}/../~${modName}`), path.normalize(`${__dirname}/${modName}`), { overwrite: true }); -fs.copySync(path.normalize(`${__dirname}/${modName}`), path.normalize(`${__dirname}/dist`)); -console.log("Build files copied."); - -// Compress the files for easy distribution. The compressed file is saved into the dist directory. When uncompressed we -// need to be sure that it includes a directory that the user can easily copy into their game mods directory. -zip({ - source: modName, - destination: `dist/${modName}.zip`, - cwd: __dirname -}).catch(function(err) -{ - console.error("A bestzip error has occurred: ", err.stack); -}).then(function() -{ - console.log(`Compressed mod package to: /dist/${modName}.zip`); - - // Now that we're done with the compression we can delete the temporary build directory. - fs.rmSync(`${__dirname}/${modName}`, { force: true, recursive: true }); - console.log("Build successful! your zip file has been created and is ready to be uploaded to hub.sp-tarkov.com/files/"); -}); \ No newline at end of file diff --git a/TypeScript/6ReferenceAnotherClass/.buildignore b/TypeScript/6ReferenceAnotherClass/.buildignore new file mode 100644 index 0000000..2cbde65 --- /dev/null +++ b/TypeScript/6ReferenceAnotherClass/.buildignore @@ -0,0 +1,20 @@ +/.buildignore +/.DS_Store +/.editorconfig +/.eslintignore +/.eslintrc.json +/.git +/.github +/.gitignore +/.gitlab +/.nvmrc +/.prettierrc +/.vscode +/build.mjs +/dist +/images +/mod.code-workspace +/node_modules +/package-lock.json +/tsconfig.json +/types diff --git a/TypeScript/6ReferenceAnotherClass/.eslintrc.json b/TypeScript/6ReferenceAnotherClass/.eslintrc.json index c505160..071a313 100644 --- a/TypeScript/6ReferenceAnotherClass/.eslintrc.json +++ b/TypeScript/6ReferenceAnotherClass/.eslintrc.json @@ -1,75 +1,98 @@ { - "root": true, - "parser": "@typescript-eslint/parser", - "plugins": [ - "@typescript-eslint" - ], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended" - ], - "rules": { - "@typescript-eslint/no-explicit-any": 0, - "@typescript-eslint/no-unused-vars": 1, - "@typescript-eslint/no-empty-interface": 0, - "@typescript-eslint/no-namespace": 0, - "@typescript-eslint/comma-dangle": 1, - "@typescript-eslint/func-call-spacing": 2, - "@typescript-eslint/quotes": 1, - "@typescript-eslint/brace-style": [ - "warn", - "allman" + "root": true, + "parser": "@typescript-eslint/parser", + "plugins": [ + "@typescript-eslint" ], - "@typescript-eslint/naming-convention": [ - "warn", - { - "selector": "default", - "format": ["camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "typeLike", - "format": ["PascalCase"] - }, - { - "selector": "objectLiteralProperty", - "format": ["PascalCase", "camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "typeProperty", - "format": ["PascalCase", "camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "enumMember", - "format": ["UPPER_CASE"] - } + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" ], - "@typescript-eslint/indent": [ - "warn", - 4 - ], - "@typescript-eslint/no-unused-expressions": [ - "warn", - { - "allowShortCircuit": false, - "allowTernary": false - } - ], - "@typescript-eslint/keyword-spacing": [ - "warn", - { - "before": true, - "after": true - } - ], - "@typescript-eslint/explicit-module-boundary-types": [ - "warn", - { - "allowArgumentsExplicitlyTypedAsAny": true - } + "rules": { + "@typescript-eslint/no-explicit-any": 0, + "@typescript-eslint/no-unused-vars": 1, + "@typescript-eslint/no-empty-interface": 0, + "@typescript-eslint/no-namespace": 0, + "@typescript-eslint/comma-dangle": 1, + "@typescript-eslint/func-call-spacing": 2, + "@typescript-eslint/quotes": 1, + "@typescript-eslint/brace-style": [ + "warn", + "allman" + ], + "@typescript-eslint/naming-convention": [ + "warn", + { + "selector": "default", + "format": [ + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "typeLike", + "format": [ + "PascalCase" + ] + }, + { + "selector": "objectLiteralProperty", + "format": [ + "PascalCase", + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "typeProperty", + "format": [ + "PascalCase", + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "enumMember", + "format": [ + "UPPER_CASE" + ] + } + ], + "@typescript-eslint/indent": [ + "warn", + 4 + ], + "@typescript-eslint/no-unused-expressions": [ + "warn", + { + "allowShortCircuit": false, + "allowTernary": false + } + ], + "@typescript-eslint/keyword-spacing": [ + "warn", + { + "before": true, + "after": true + } + ], + "@typescript-eslint/explicit-module-boundary-types": [ + "warn", + { + "allowArgumentsExplicitlyTypedAsAny": true + } + ] + }, + "overrides": [ + { + "files": [ + "*.mjs", + "*.ts" + ], + "env": { + "node": true + } + } ] - } } \ No newline at end of file diff --git a/TypeScript/6ReferenceAnotherClass/README.md b/TypeScript/6ReferenceAnotherClass/README.md index 434d063..8cba28e 100644 --- a/TypeScript/6ReferenceAnotherClass/README.md +++ b/TypeScript/6ReferenceAnotherClass/README.md @@ -1,64 +1,66 @@ +# Welcome to the SPT-AKI Modding Project -This project was created to automate most parts of building and setting up an environment. +This project is designed to streamline the initial setup process for building and creating mods in the SPT-AKI environment. Follow this guide to set up your environment efficiently. -## **NodeJS:** +## **Table of Contents** +- [NodeJS Setup](#nodejs-setup) +- [IDE Setup](#ide-setup) +- [Workspace Configuration](#workspace-configuration) +- [Environment Setup](#environment-setup) +- [Essential Concepts](#essential-concepts) +- [Coding Guidelines](#coding-guidelines) +- [Distribution Guidelines](#distribution-guidelines) -The first step would be to install nodejs on your pc, the version you NEED is **16.17.1** +## **NodeJS Setup** -That version is the one that has been used to test the mod templates and build scripts. +Before you begin, ensure to install NodeJS version `v16.17.1`, which has been tested thoroughly with our mod templates and build scripts. Download it from the [official NodeJS website](https://nodejs.org/). -It can be downloaded from here: https://nodejs.org/dist/v16.17.1/node-v16.17.1-x64.msi +After installation, it's advised to reboot your system. -A system reboot may be needed after install. +## **IDE Setup** -## **IDE:** +For this project, you can work with either [VSCodium](https://vscodium.com/) or [VSCode](https://code.visualstudio.com/). However, we strongly recommend using VSCode, as all development and testing have been carried out using this IDE, ensuring a smoother experience and compatibility with the project setups. Either way, we have a prepared a workspace file to assist you in setting up your environment. -The second step is having an IDE ready. We've setup a VSCodium workspace file to help with this. +## **Workspace Configuration** -You CAN use Visual Studio Code if you so desire, just keep in mind that our devs tests on the mod files was done using VSCode. +With NodeJS and your chosen IDE ready, initiate the `mod.code-workspace` file using your IDE: -You can get VSCode here: https://code.visualstudio.com/ +> File -> Open Workspace from File... -## **Workspace:** +Upon project loading, consider installing recommended plugins like the ESLint plugin. -Once you have NodeJS and VSCode/VSCodium ready, open the mod.code-workspace file with VSCode (File->Open Workspace from File...). +## **Environment Setup** -Once the project loads you may be recommended to install the ESLint plugin. This is HIGHLY recommended. +An automated task is available to configure your environment for Typescript utilization: -## **Environment Setup:** +> Terminal -> Run Task... -> Show All Tasks... -> npm: install -There is a task that will automatically setup your environment to use typescript. +Note: Preserve the `node_modules` folder as it contains necessary dependencies for Typescript and other functionalities. -To run it, you just need to go to: +## **Essential Concepts** -> Terminal->Run Task...->Show All Tasks...->npm: install +Prioritize understanding Dependency Injection and Inversion of Control, the architectural principles SPT-AKI adopts. Comprehensive guidelines will be available on the hub upon release. -After running this task, your environment will be ready to start coding. +Some resources to get you started: + - [A quick intro to Dependency Injection](https://www.freecodecamp.org/news/a-quick-intro-to-dependency-injection-what-it-is-and-when-to-use-it-7578c84fa88f/) + - [Understanding Inversion of Control (IoC) Principle](https://medium.com/@amitkma/understanding-inversion-of-control-ioc-principle-163b1dc97454) -DO NOT remove the node_modules folder, this is an auto generated directory that has the required dependencies to be able to use typescript and more. +## **Coding Guidelines** -## **IMPORTANT:** +Focus your mod development around the `mod.ts` file. In the `package.json` file, only alter these properties: `"name"`, `"version"`, `"license"`, `"author"`, and `"akiVersion"`. -Before starting to work on your mod, we suggest you read about Dependency Injection and Inversion of Control as this is the adopted architecture SPT-AKI has adopted. +New to Typescript? Find comprehensive documentation on the [official website](https://www.typescriptlang.org/docs/). -It will be difficult to understand some of the problems you may be having if you dont understand the basics of it. +## **Distribution Guidelines** -A guide explaining all the essentials will be available on the hub on release for you to read about. +Automated tasks are set up to bundle all necessary files for your mod to function in SPT-AKI: -## **Coding:** +> Terminal -> Run Task... -> Show All Tasks... -> npm: build -All your work should be centered around the mod.ts file as an entry point. -You can ONLY change the following properties from the package.json file: `"name"`, `"version"`, `"license"`: `"MIT"`, `"author"`, `"akiVersion"`. +The ZIP output, located in the `dist` directory, contains all required files. Ensure all files are included and modify the `.buildignore` file as needed. This ZIP file is your uploadable asset for the hub. -If you have never used typescript before, you can read about it here: https://www.typescriptlang.org/docs/ +## **Conclusion** -## **Distributing your mod:** +With this setup, you're ready to begin modding with SPT-AKI. If you run into any trouble be sure to check out the [modding documentation on the hub](https://hub.sp-tarkov.com/doc/lexicon/66-modding/). If you really get stuck feel free to join us in the [#mods-development](https://discord.com/channels/875684761291599922/875803116409323562) official Discord channel. -The project has been set up with an automatic task that will copy and zip ALL required files for your mod to work on SPT-AKI. -To run this task you just need to go to: - -> Terminal->Run Task...->Show All Tasks...->npm: build:zip - -The output will be a mod.zip file that will appear on the root of the project. - -Always verify that all files were included into the zip file. \ No newline at end of file +Build something awesome! diff --git a/TypeScript/6ReferenceAnotherClass/build.mjs b/TypeScript/6ReferenceAnotherClass/build.mjs new file mode 100644 index 0000000..d00117c --- /dev/null +++ b/TypeScript/6ReferenceAnotherClass/build.mjs @@ -0,0 +1,383 @@ +#!/usr/bin/env node + +/** + * Build Script + * + * This script automates the build process for server-side SPT mod projects, facilitating the creation of distributable + * mod packages. It performs a series of operations as outlined below: + * - Loads the .buildignore file, which is used to list files that should be ignored during the build process. + * - Loads the package.json to get project details so a descriptive name can be created for the mod package. + * - Creates a distribution directory and a temporary working directory. + * - Copies files to the temporary directory while respecting the .buildignore rules. + * - Creates a zip archive of the project files. + * - Moves the zip file to the root of the distribution directory. + * - Cleans up the temporary directory. + * + * It's typical that this script be customized to suit the needs of each project. For example, the script can be updated + * to perform additional operations, such as moving the mod package to a specific location or uploading it to a server. + * This script is intended to be a starting point for developers to build upon. + * + * Usage: + * - Run this script using npm: `npm run build` + * - Use `npm run buildinfo` for detailed logging. + * + * Note: + * - Ensure that all necessary Node.js modules are installed before running the script: `npm install` + * - The script reads configurations from the `package.json` and `.buildignore` files; ensure they are correctly set up. + * + * @author Refringe + * @version v1.0.0 + */ + +import fs from "fs-extra"; +import os from "os"; +import path from "path"; +import { fileURLToPath } from "url"; +import { dirname } from "path"; +import ignore from "ignore"; +import archiver from "archiver"; +import winston from "winston"; + +// Get the command line arguments to determine whether to use verbose logging. +const args = process.argv.slice(2); +const verbose = args.includes("--verbose") || args.includes("-v"); + +// Configure the Winston logger to use colours. +const logColors = { + error: "red", + warn: "yellow", + info: "grey", + success: "green", +}; +winston.addColors(logColors); + +// Create a logger instance to log build progress. Configure the logger levels to allow for different levels of logging +// based on the verbosity flag, and set the console transport to log messages of the appropriate level. +const logger = winston.createLogger({ + levels: { + error: 0, + warn: 1, + success: 2, + info: 3, + }, + format: winston.format.combine( + winston.format.colorize(), + winston.format.printf(info => { + return `${info.level}: ${info.message}`; + }) + ), + transports: [ + new winston.transports.Console({ + level: verbose ? "info" : "success", + }), + ], +}); + +/** + * The main function orchestrates the build process for creating a distributable mod package. It leverages a series of + * helper functions to perform various tasks such as loading configuration files, setting up directories, copying files + * according to `.buildignore` rules, and creating a ZIP archive of the project files. + * + * Utilizes the Winston logger to provide information on the build status at different stages of the process. + * + * @returns {void} + */ +async function main() { + // Get the current directory where the script is being executed + const currentDir = getCurrentDirectory(); + + // Defining at this scope because we need to use it in the finally block. + let projectDir; + + try { + // Load the .buildignore file to set up an ignore handler for the build process. + const buildIgnorePatterns = await loadBuildIgnoreFile(currentDir); + + // Load the package.json file to get project details. + const packageJson = await loadPackageJson(currentDir); + + // Create a descriptive name for the mod package. + const projectName = createProjectName(packageJson); + logger.log("success", `Project name created: ${projectName}`); + + // Remove the old distribution directory and create a fresh one. + const distDir = await removeOldDistDirectory(currentDir); + logger.log("info", "Distribution directory successfully cleaned."); + + // Create a temporary working directory to perform the build operations. + projectDir = await createTemporaryDirectoryWithProjectName(projectName); + logger.log("success", "Temporary working directory successfully created."); + logger.log("info", projectDir); + + // Copy files to the temporary directory while respecting the .buildignore rules. + logger.log("info", "Beginning copy operation using .buildignore file..."); + await copyFiles(currentDir, projectDir, buildIgnorePatterns); + logger.log("success", "Files successfully copied to temporary directory."); + + // Create a zip archive of the project files. + logger.log("info", "Beginning folder compression..."); + const zipFilePath = path.join(path.dirname(projectDir), `${projectName}.zip`); + await createZipFile(projectDir, zipFilePath, "user/mods/" + projectName); + logger.log("success", "Archive successfully created."); + logger.log("info", zipFilePath); + + // Move the zip file inside of the project directory, within the temporary working directory. + const zipFileInProjectDir = path.join(projectDir, `${projectName}.zip`); + await fs.move(zipFilePath, zipFileInProjectDir); + logger.log("success", "Archive successfully moved."); + logger.log("info", zipFileInProjectDir); + + // Move the temporary directory into the distribution directory. + await fs.move(projectDir, distDir); + logger.log("success", "Temporary directory successfully moved into project distribution directory."); + + // Log the success message. Write out the path to the mod package. + logger.log("success", "------------------------------------"); + logger.log("success", "Build script completed successfully!"); + logger.log("success", "Your mod package has been created in the 'dist' directory:"); + logger.log("success", `/${path.relative(process.cwd(), path.join(distDir, `${projectName}.zip`))}`); + logger.log("success", "------------------------------------"); + if (!verbose) { + logger.log("success", "To see a detailed build log, use `npm run buildinfo`."); + logger.log("success", "------------------------------------"); + } + } catch (err) { + // If any of the file operations fail, log the error. + logger.log("error", "An error occurred: " + err); + } finally { + // Clean up the temporary directory, even if the build fails. + if (projectDir) { + try { + await fs.promises.rm(projectDir, { force: true, recursive: true }); + logger.log("info", "Cleaned temporary directory."); + } catch (err) { + logger.log("error", "Failed to clean temporary directory: " + err); + } + } + } +} + +/** + * Retrieves the current working directory where the script is being executed. This directory is used as a reference + * point for various file operations throughout the build process, ensuring that paths are resolved correctly regardless + * of the location from which the script is invoked. + * + * @returns {string} The absolute path of the current working directory. + */ +function getCurrentDirectory() { + return dirname(fileURLToPath(import.meta.url)); +} + +/** + * Loads the `.buildignore` file and sets up an ignore handler using the `ignore` module. The `.buildignore` file + * contains a list of patterns describing files and directories that should be ignored during the build process. The + * ignore handler created by this method is used to filter files and directories when copying them to the temporary + * directory, ensuring that only necessary files are included in the final mod package. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to an ignore handler. + */ +async function loadBuildIgnoreFile(currentDir) { + const buildIgnorePath = path.join(currentDir, ".buildignore"); + + try { + // Attempt to read the contents of the .buildignore file asynchronously. + const fileContent = await fs.promises.readFile(buildIgnorePath, "utf-8"); + + // Return a new ignore instance and add the rules from the .buildignore file (split by newlines). + return ignore().add(fileContent.split("\n")); + } catch (err) { + logger.log("warn", "Failed to read .buildignore file. No files or directories will be ignored."); + + // Return an empty ignore instance, ensuring the build process can continue. + return ignore(); + } +} + +/** + * Loads the `package.json` file and returns its content as a JSON object. The `package.json` file contains important + * project details such as the name and version, which are used in later stages of the build process to create a + * descriptive name for the mod package. The method reads the file from the current working directory, ensuring that it + * accurately reflects the current state of the project. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to a JSON object containing the contents of the `package.json`. + */ +async function loadPackageJson(currentDir) { + const packageJsonPath = path.join(currentDir, "package.json"); + + // Read the contents of the package.json file asynchronously as a UTF-8 string. + const packageJsonContent = await fs.promises.readFile(packageJsonPath, "utf-8"); + + return JSON.parse(packageJsonContent); +} + +/** + * Constructs a descriptive name for the mod package using details from the `package.json` file. The name is created by + * concatenating the project name, version, and a timestamp, resulting in a unique and descriptive file name for each + * build. This name is used as the base name for the temporary working directory and the final ZIP archive, helping to + * identify different versions of the mod package easily. + * + * @param {Object} packageJson - A JSON object containing the contents of the `package.json` file. + * @returns {string} A string representing the constructed project name. + */ +function createProjectName(packageJson) { + // Remove any non-alphanumeric characters from the author and name. + const author = packageJson.author.replace(/\W/g, ""); + const name = packageJson.name.replace(/\W/g, ""); + const version = packageJson.version; + + // Ensure the name is lowercase, as per the package.json specification. + return `${author}-${name}-${version}`.toLowerCase(); +} + +/** + * Defines the location of the distribution directory where the final mod package will be stored and deletes any + * existing distribution directory to ensure a clean slate for the build process. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to the absolute path to the distribution directory. + */ +async function removeOldDistDirectory(projectDir) { + const distPath = path.join(projectDir, "dist"); + await fs.remove(distPath); + return distPath; +} + +/** + * Creates a temporary working directory using the project name. This directory serves as a staging area where project + * files are gathered before being archived into the final mod package. The method constructs a unique directory path + * by appending the project name to a base temporary directory path, ensuring that each build has its own isolated + * working space. This approach facilitates clean and organized build processes, avoiding potential conflicts with other + * builds. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @param {string} projectName - The constructed project name, used to create a unique path for the temporary directory. + * @returns {Promise} A promise that resolves to the absolute path of the newly created temporary directory. + */ +async function createTemporaryDirectoryWithProjectName(projectName) { + // Create a new directory in the system's temporary folder to hold the project files. + const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "spt-mod-build-")); + + // Create a subdirectory within the temporary directory using the project name for this specific build. + const projectDir = path.join(tempDir, projectName); + await fs.ensureDir(projectDir); + + return projectDir; +} + +/** + * Copies the project files to the temporary directory while respecting the rules defined in the `.buildignore` file. + * The method is recursive, iterating over all files and directories in the source directory and using the ignore + * handler to filter out files and directories that match the patterns defined in the `.buildignore` file. This ensures + * that only the necessary files are included in the final mod package, adhering to the specifications defined by the + * developer in the `.buildignore` file. + * + * The copy operations are delayed and executed in parallel to improve efficiency and reduce the build time. This is + * achieved by creating an array of copy promises and awaiting them all at the end of the function. + * + * @param {string} sourceDirectory - The absolute path of the current working directory. + * @param {string} destinationDirectory - The absolute path of the temporary directory where the files will be copied. + * @param {Ignore} ignoreHandler - The ignore handler created from the `.buildignore` file. + * @returns {Promise} A promise that resolves when all copy operations are completed successfully. + */ +async function copyFiles(srcDir, destDir, ignoreHandler) { + try { + // Read the contents of the source directory to get a list of entries (files and directories). + const entries = await fs.promises.readdir(srcDir, { withFileTypes: true }); + + // Initialize an array to hold the promises returned by recursive calls to copyFiles and copyFile operations. + const copyOperations = []; + + for (const entry of entries) { + // Define the source and destination paths for each entry. + const srcPath = path.join(srcDir, entry.name); + const destPath = path.join(destDir, entry.name); + + // Get the relative path of the source file to check against the ignore handler. + const relativePath = path.relative(process.cwd(), srcPath); + + // If the ignore handler dictates that this file should be ignored, skip to the next iteration. + if (ignoreHandler.ignores(relativePath)) { + logger.log("info", `Ignored: /${path.relative(process.cwd(), srcPath)}`); + continue; + } + + if (entry.isDirectory()) { + // If the entry is a directory, create the corresponding temporary directory and make a recursive call + // to copyFiles to handle copying the contents of the directory. + await fs.ensureDir(destPath); + copyOperations.push(copyFiles(srcPath, destPath, ignoreHandler)); + } else { + // If the entry is a file, add a copyFile operation to the copyOperations array and log the event when + // the operation is successful. + copyOperations.push( + fs.copy(srcPath, destPath).then(() => { + logger.log("info", `Copied: /${path.relative(process.cwd(), srcPath)}`); + }) + ); + } + } + + // Await all copy operations to ensure all files and directories are copied before exiting the function. + await Promise.all(copyOperations); + } catch (err) { + // Log an error message if any error occurs during the copy process. + logger.log("error", "Error copying files: " + err); + } +} + +/** + * Creates a ZIP archive of the project files located in the temporary directory. The method uses the `archiver` module + * to create a ZIP file, which includes all the files that have been copied to the temporary directory during the build + * process. The ZIP file is named using the project name, helping to identify the contents of the archive easily. + * + * @param {string} directoryPath - The absolute path of the temporary directory containing the project files. + * @param {string} projectName - The constructed project name, used to name the ZIP file. + * @returns {Promise} A promise that resolves to the absolute path of the created ZIP file. + */ +async function createZipFile(directoryToZip, zipFilePath, containerDirName) { + return new Promise((resolve, reject) => { + // Create a write stream to the specified ZIP file path. + const output = fs.createWriteStream(zipFilePath); + + // Create a new archiver instance with ZIP format and maximum compression level. + const archive = archiver("zip", { + zlib: { level: 9 }, + }); + + // Set up an event listener for the 'close' event to resolve the promise when the archiver has finalized. + output.on("close", function () { + logger.log("info", "Archiver has finalized. The output and the file descriptor have closed."); + resolve(); + }); + + // Set up an event listener for the 'warning' event to handle warnings appropriately, logging them or rejecting + // the promise based on the error code. + archive.on("warning", function (err) { + if (err.code === "ENOENT") { + logger.log("warn", `Archiver issued a warning: ${err.code} - ${err.message}`); + } else { + reject(err); + } + }); + + // Set up an event listener for the 'error' event to reject the promise if any error occurs during archiving. + archive.on("error", function (err) { + reject(err); + }); + + // Pipe archive data to the file. + archive.pipe(output); + + // Add the directory to the archive, under the provided directory name. + archive.directory(directoryToZip, containerDirName); + + // Finalize the archive, indicating that no more files will be added and triggering the 'close' event once all + // data has been written. + archive.finalize(); + }); +} + +// Engage! +main(); diff --git a/TypeScript/6ReferenceAnotherClass/package.json b/TypeScript/6ReferenceAnotherClass/package.json index 8e0ca6b..59e2579 100644 --- a/TypeScript/6ReferenceAnotherClass/package.json +++ b/TypeScript/6ReferenceAnotherClass/package.json @@ -7,17 +7,20 @@ "akiVersion": "~3.7", "scripts": { "setup": "npm i", - "build": "node ./packageBuild.ts" + "build": "node ./build.mjs", + "buildinfo": "node ./build.mjs --verbose" }, "devDependencies": { "@types/node": "20.4.5", "@typescript-eslint/eslint-plugin": "6.2.0", "@typescript-eslint/parser": "6.2.0", - "bestzip": "2.2.1", + "archiver": "^6.0", "eslint": "8.46.0", - "fs-extra": "11.1.1", - "glob": "10.3.3", + "fs-extra": "^11.1", + "ignore": "^5.2", + "os": "^0.1", "tsyringe": "4.8.0", - "typescript": "5.1.6" + "typescript": "5.1.6", + "winston": "^3.9" } -} \ No newline at end of file +} diff --git a/TypeScript/6ReferenceAnotherClass/packageBuild.ts b/TypeScript/6ReferenceAnotherClass/packageBuild.ts deleted file mode 100644 index d76ddd8..0000000 --- a/TypeScript/6ReferenceAnotherClass/packageBuild.ts +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env node - -// This is a simple script used to build a mod package. The script will copy necessary files to the build directory -// and compress the build directory into a zip file that can be easily shared. - -const fs = require("fs-extra"); -const glob = require("glob"); -const zip = require('bestzip'); -const path = require("path"); -const minimatch = require('minimatch'); - -// Load the package.json file to get some information about the package so we can name things appropriately. This is -// atypical, and you would never do this in a production environment, but this script is only used for development so -// it's fine in this case. Some of these values are stored in environment variables, but those differ between node -// versions; the 'author' value is not available after node v14. -const { author, name:packageName, version } = require("./package.json"); - -// Generate the name of the package, stripping out all non-alphanumeric characters in the 'author' and 'name'. -const modName = `${author.replace(/[^a-z0-9]/gi, "")}-${packageName.replace(/[^a-z0-9]/gi, "")}-${version}`; -console.log(`Generated package name: ${modName}`); - -// Delete the old build directory and compressed package file. -fs.rmSync(`${__dirname}/dist`, { force: true, recursive: true }); -console.log("Previous build files deleted."); - -// Generate a list of files that should not be copied over into the distribution directory. This is a blacklist to ensure -// we always copy over additional files and directories that authors may have added to their project. This may need to be -// expanded upon by the mod author to allow for node modules that are used within the mod; example commented out below. -const ignoreList = [ - "node_modules/", - // "node_modules/!(weighted|glob)", // Instead of excluding the entire node_modules directory, allow two node modules. - "src/**/*.js", - "types/", - ".git/", - ".gitea/", - ".eslintignore", - ".eslintrc.json", - ".gitignore", - ".DS_Store", - "packageBuild.ts", - "mod.code-workspace", - "package-lock.json", - "tsconfig.json" -]; -const exclude = glob.sync(`{${ignoreList.join(",")}}`, { realpath: true, dot: true }); - -fs.copySync(__dirname, path.normalize(`${__dirname}/../~${modName}`), {filter: (src) => -{ - const relativePath = path.relative(__dirname, src); - const shouldExclude = exclude.some((pattern) => minimatch(relativePath, pattern)); - console.log(`${relativePath} - Excluded: ${shouldExclude}`); - return !shouldExclude; -},}); - -fs.moveSync(path.normalize(`${__dirname}/../~${modName}`), path.normalize(`${__dirname}/${modName}`), { overwrite: true }); -fs.copySync(path.normalize(`${__dirname}/${modName}`), path.normalize(`${__dirname}/dist`)); -console.log("Build files copied."); - -// Compress the files for easy distribution. The compressed file is saved into the dist directory. When uncompressed we -// need to be sure that it includes a directory that the user can easily copy into their game mods directory. -zip({ - source: modName, - destination: `dist/${modName}.zip`, - cwd: __dirname -}).catch(function(err) -{ - console.error("A bestzip error has occurred: ", err.stack); -}).then(function() -{ - console.log(`Compressed mod package to: /dist/${modName}.zip`); - - // Now that we're done with the compression we can delete the temporary build directory. - fs.rmSync(`${__dirname}/${modName}`, { force: true, recursive: true }); - console.log("Build successful! your zip file has been created and is ready to be uploaded to hub.sp-tarkov.com/files/"); -}); \ No newline at end of file diff --git a/TypeScript/7OnLoadHook/.buildignore b/TypeScript/7OnLoadHook/.buildignore new file mode 100644 index 0000000..2cbde65 --- /dev/null +++ b/TypeScript/7OnLoadHook/.buildignore @@ -0,0 +1,20 @@ +/.buildignore +/.DS_Store +/.editorconfig +/.eslintignore +/.eslintrc.json +/.git +/.github +/.gitignore +/.gitlab +/.nvmrc +/.prettierrc +/.vscode +/build.mjs +/dist +/images +/mod.code-workspace +/node_modules +/package-lock.json +/tsconfig.json +/types diff --git a/TypeScript/7OnLoadHook/.eslintrc.json b/TypeScript/7OnLoadHook/.eslintrc.json index c505160..071a313 100644 --- a/TypeScript/7OnLoadHook/.eslintrc.json +++ b/TypeScript/7OnLoadHook/.eslintrc.json @@ -1,75 +1,98 @@ { - "root": true, - "parser": "@typescript-eslint/parser", - "plugins": [ - "@typescript-eslint" - ], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended" - ], - "rules": { - "@typescript-eslint/no-explicit-any": 0, - "@typescript-eslint/no-unused-vars": 1, - "@typescript-eslint/no-empty-interface": 0, - "@typescript-eslint/no-namespace": 0, - "@typescript-eslint/comma-dangle": 1, - "@typescript-eslint/func-call-spacing": 2, - "@typescript-eslint/quotes": 1, - "@typescript-eslint/brace-style": [ - "warn", - "allman" + "root": true, + "parser": "@typescript-eslint/parser", + "plugins": [ + "@typescript-eslint" ], - "@typescript-eslint/naming-convention": [ - "warn", - { - "selector": "default", - "format": ["camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "typeLike", - "format": ["PascalCase"] - }, - { - "selector": "objectLiteralProperty", - "format": ["PascalCase", "camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "typeProperty", - "format": ["PascalCase", "camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "enumMember", - "format": ["UPPER_CASE"] - } + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" ], - "@typescript-eslint/indent": [ - "warn", - 4 - ], - "@typescript-eslint/no-unused-expressions": [ - "warn", - { - "allowShortCircuit": false, - "allowTernary": false - } - ], - "@typescript-eslint/keyword-spacing": [ - "warn", - { - "before": true, - "after": true - } - ], - "@typescript-eslint/explicit-module-boundary-types": [ - "warn", - { - "allowArgumentsExplicitlyTypedAsAny": true - } + "rules": { + "@typescript-eslint/no-explicit-any": 0, + "@typescript-eslint/no-unused-vars": 1, + "@typescript-eslint/no-empty-interface": 0, + "@typescript-eslint/no-namespace": 0, + "@typescript-eslint/comma-dangle": 1, + "@typescript-eslint/func-call-spacing": 2, + "@typescript-eslint/quotes": 1, + "@typescript-eslint/brace-style": [ + "warn", + "allman" + ], + "@typescript-eslint/naming-convention": [ + "warn", + { + "selector": "default", + "format": [ + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "typeLike", + "format": [ + "PascalCase" + ] + }, + { + "selector": "objectLiteralProperty", + "format": [ + "PascalCase", + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "typeProperty", + "format": [ + "PascalCase", + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "enumMember", + "format": [ + "UPPER_CASE" + ] + } + ], + "@typescript-eslint/indent": [ + "warn", + 4 + ], + "@typescript-eslint/no-unused-expressions": [ + "warn", + { + "allowShortCircuit": false, + "allowTernary": false + } + ], + "@typescript-eslint/keyword-spacing": [ + "warn", + { + "before": true, + "after": true + } + ], + "@typescript-eslint/explicit-module-boundary-types": [ + "warn", + { + "allowArgumentsExplicitlyTypedAsAny": true + } + ] + }, + "overrides": [ + { + "files": [ + "*.mjs", + "*.ts" + ], + "env": { + "node": true + } + } ] - } } \ No newline at end of file diff --git a/TypeScript/7OnLoadHook/README.md b/TypeScript/7OnLoadHook/README.md index 434d063..8cba28e 100644 --- a/TypeScript/7OnLoadHook/README.md +++ b/TypeScript/7OnLoadHook/README.md @@ -1,64 +1,66 @@ +# Welcome to the SPT-AKI Modding Project -This project was created to automate most parts of building and setting up an environment. +This project is designed to streamline the initial setup process for building and creating mods in the SPT-AKI environment. Follow this guide to set up your environment efficiently. -## **NodeJS:** +## **Table of Contents** +- [NodeJS Setup](#nodejs-setup) +- [IDE Setup](#ide-setup) +- [Workspace Configuration](#workspace-configuration) +- [Environment Setup](#environment-setup) +- [Essential Concepts](#essential-concepts) +- [Coding Guidelines](#coding-guidelines) +- [Distribution Guidelines](#distribution-guidelines) -The first step would be to install nodejs on your pc, the version you NEED is **16.17.1** +## **NodeJS Setup** -That version is the one that has been used to test the mod templates and build scripts. +Before you begin, ensure to install NodeJS version `v16.17.1`, which has been tested thoroughly with our mod templates and build scripts. Download it from the [official NodeJS website](https://nodejs.org/). -It can be downloaded from here: https://nodejs.org/dist/v16.17.1/node-v16.17.1-x64.msi +After installation, it's advised to reboot your system. -A system reboot may be needed after install. +## **IDE Setup** -## **IDE:** +For this project, you can work with either [VSCodium](https://vscodium.com/) or [VSCode](https://code.visualstudio.com/). However, we strongly recommend using VSCode, as all development and testing have been carried out using this IDE, ensuring a smoother experience and compatibility with the project setups. Either way, we have a prepared a workspace file to assist you in setting up your environment. -The second step is having an IDE ready. We've setup a VSCodium workspace file to help with this. +## **Workspace Configuration** -You CAN use Visual Studio Code if you so desire, just keep in mind that our devs tests on the mod files was done using VSCode. +With NodeJS and your chosen IDE ready, initiate the `mod.code-workspace` file using your IDE: -You can get VSCode here: https://code.visualstudio.com/ +> File -> Open Workspace from File... -## **Workspace:** +Upon project loading, consider installing recommended plugins like the ESLint plugin. -Once you have NodeJS and VSCode/VSCodium ready, open the mod.code-workspace file with VSCode (File->Open Workspace from File...). +## **Environment Setup** -Once the project loads you may be recommended to install the ESLint plugin. This is HIGHLY recommended. +An automated task is available to configure your environment for Typescript utilization: -## **Environment Setup:** +> Terminal -> Run Task... -> Show All Tasks... -> npm: install -There is a task that will automatically setup your environment to use typescript. +Note: Preserve the `node_modules` folder as it contains necessary dependencies for Typescript and other functionalities. -To run it, you just need to go to: +## **Essential Concepts** -> Terminal->Run Task...->Show All Tasks...->npm: install +Prioritize understanding Dependency Injection and Inversion of Control, the architectural principles SPT-AKI adopts. Comprehensive guidelines will be available on the hub upon release. -After running this task, your environment will be ready to start coding. +Some resources to get you started: + - [A quick intro to Dependency Injection](https://www.freecodecamp.org/news/a-quick-intro-to-dependency-injection-what-it-is-and-when-to-use-it-7578c84fa88f/) + - [Understanding Inversion of Control (IoC) Principle](https://medium.com/@amitkma/understanding-inversion-of-control-ioc-principle-163b1dc97454) -DO NOT remove the node_modules folder, this is an auto generated directory that has the required dependencies to be able to use typescript and more. +## **Coding Guidelines** -## **IMPORTANT:** +Focus your mod development around the `mod.ts` file. In the `package.json` file, only alter these properties: `"name"`, `"version"`, `"license"`, `"author"`, and `"akiVersion"`. -Before starting to work on your mod, we suggest you read about Dependency Injection and Inversion of Control as this is the adopted architecture SPT-AKI has adopted. +New to Typescript? Find comprehensive documentation on the [official website](https://www.typescriptlang.org/docs/). -It will be difficult to understand some of the problems you may be having if you dont understand the basics of it. +## **Distribution Guidelines** -A guide explaining all the essentials will be available on the hub on release for you to read about. +Automated tasks are set up to bundle all necessary files for your mod to function in SPT-AKI: -## **Coding:** +> Terminal -> Run Task... -> Show All Tasks... -> npm: build -All your work should be centered around the mod.ts file as an entry point. -You can ONLY change the following properties from the package.json file: `"name"`, `"version"`, `"license"`: `"MIT"`, `"author"`, `"akiVersion"`. +The ZIP output, located in the `dist` directory, contains all required files. Ensure all files are included and modify the `.buildignore` file as needed. This ZIP file is your uploadable asset for the hub. -If you have never used typescript before, you can read about it here: https://www.typescriptlang.org/docs/ +## **Conclusion** -## **Distributing your mod:** +With this setup, you're ready to begin modding with SPT-AKI. If you run into any trouble be sure to check out the [modding documentation on the hub](https://hub.sp-tarkov.com/doc/lexicon/66-modding/). If you really get stuck feel free to join us in the [#mods-development](https://discord.com/channels/875684761291599922/875803116409323562) official Discord channel. -The project has been set up with an automatic task that will copy and zip ALL required files for your mod to work on SPT-AKI. -To run this task you just need to go to: - -> Terminal->Run Task...->Show All Tasks...->npm: build:zip - -The output will be a mod.zip file that will appear on the root of the project. - -Always verify that all files were included into the zip file. \ No newline at end of file +Build something awesome! diff --git a/TypeScript/7OnLoadHook/build.mjs b/TypeScript/7OnLoadHook/build.mjs new file mode 100644 index 0000000..d00117c --- /dev/null +++ b/TypeScript/7OnLoadHook/build.mjs @@ -0,0 +1,383 @@ +#!/usr/bin/env node + +/** + * Build Script + * + * This script automates the build process for server-side SPT mod projects, facilitating the creation of distributable + * mod packages. It performs a series of operations as outlined below: + * - Loads the .buildignore file, which is used to list files that should be ignored during the build process. + * - Loads the package.json to get project details so a descriptive name can be created for the mod package. + * - Creates a distribution directory and a temporary working directory. + * - Copies files to the temporary directory while respecting the .buildignore rules. + * - Creates a zip archive of the project files. + * - Moves the zip file to the root of the distribution directory. + * - Cleans up the temporary directory. + * + * It's typical that this script be customized to suit the needs of each project. For example, the script can be updated + * to perform additional operations, such as moving the mod package to a specific location or uploading it to a server. + * This script is intended to be a starting point for developers to build upon. + * + * Usage: + * - Run this script using npm: `npm run build` + * - Use `npm run buildinfo` for detailed logging. + * + * Note: + * - Ensure that all necessary Node.js modules are installed before running the script: `npm install` + * - The script reads configurations from the `package.json` and `.buildignore` files; ensure they are correctly set up. + * + * @author Refringe + * @version v1.0.0 + */ + +import fs from "fs-extra"; +import os from "os"; +import path from "path"; +import { fileURLToPath } from "url"; +import { dirname } from "path"; +import ignore from "ignore"; +import archiver from "archiver"; +import winston from "winston"; + +// Get the command line arguments to determine whether to use verbose logging. +const args = process.argv.slice(2); +const verbose = args.includes("--verbose") || args.includes("-v"); + +// Configure the Winston logger to use colours. +const logColors = { + error: "red", + warn: "yellow", + info: "grey", + success: "green", +}; +winston.addColors(logColors); + +// Create a logger instance to log build progress. Configure the logger levels to allow for different levels of logging +// based on the verbosity flag, and set the console transport to log messages of the appropriate level. +const logger = winston.createLogger({ + levels: { + error: 0, + warn: 1, + success: 2, + info: 3, + }, + format: winston.format.combine( + winston.format.colorize(), + winston.format.printf(info => { + return `${info.level}: ${info.message}`; + }) + ), + transports: [ + new winston.transports.Console({ + level: verbose ? "info" : "success", + }), + ], +}); + +/** + * The main function orchestrates the build process for creating a distributable mod package. It leverages a series of + * helper functions to perform various tasks such as loading configuration files, setting up directories, copying files + * according to `.buildignore` rules, and creating a ZIP archive of the project files. + * + * Utilizes the Winston logger to provide information on the build status at different stages of the process. + * + * @returns {void} + */ +async function main() { + // Get the current directory where the script is being executed + const currentDir = getCurrentDirectory(); + + // Defining at this scope because we need to use it in the finally block. + let projectDir; + + try { + // Load the .buildignore file to set up an ignore handler for the build process. + const buildIgnorePatterns = await loadBuildIgnoreFile(currentDir); + + // Load the package.json file to get project details. + const packageJson = await loadPackageJson(currentDir); + + // Create a descriptive name for the mod package. + const projectName = createProjectName(packageJson); + logger.log("success", `Project name created: ${projectName}`); + + // Remove the old distribution directory and create a fresh one. + const distDir = await removeOldDistDirectory(currentDir); + logger.log("info", "Distribution directory successfully cleaned."); + + // Create a temporary working directory to perform the build operations. + projectDir = await createTemporaryDirectoryWithProjectName(projectName); + logger.log("success", "Temporary working directory successfully created."); + logger.log("info", projectDir); + + // Copy files to the temporary directory while respecting the .buildignore rules. + logger.log("info", "Beginning copy operation using .buildignore file..."); + await copyFiles(currentDir, projectDir, buildIgnorePatterns); + logger.log("success", "Files successfully copied to temporary directory."); + + // Create a zip archive of the project files. + logger.log("info", "Beginning folder compression..."); + const zipFilePath = path.join(path.dirname(projectDir), `${projectName}.zip`); + await createZipFile(projectDir, zipFilePath, "user/mods/" + projectName); + logger.log("success", "Archive successfully created."); + logger.log("info", zipFilePath); + + // Move the zip file inside of the project directory, within the temporary working directory. + const zipFileInProjectDir = path.join(projectDir, `${projectName}.zip`); + await fs.move(zipFilePath, zipFileInProjectDir); + logger.log("success", "Archive successfully moved."); + logger.log("info", zipFileInProjectDir); + + // Move the temporary directory into the distribution directory. + await fs.move(projectDir, distDir); + logger.log("success", "Temporary directory successfully moved into project distribution directory."); + + // Log the success message. Write out the path to the mod package. + logger.log("success", "------------------------------------"); + logger.log("success", "Build script completed successfully!"); + logger.log("success", "Your mod package has been created in the 'dist' directory:"); + logger.log("success", `/${path.relative(process.cwd(), path.join(distDir, `${projectName}.zip`))}`); + logger.log("success", "------------------------------------"); + if (!verbose) { + logger.log("success", "To see a detailed build log, use `npm run buildinfo`."); + logger.log("success", "------------------------------------"); + } + } catch (err) { + // If any of the file operations fail, log the error. + logger.log("error", "An error occurred: " + err); + } finally { + // Clean up the temporary directory, even if the build fails. + if (projectDir) { + try { + await fs.promises.rm(projectDir, { force: true, recursive: true }); + logger.log("info", "Cleaned temporary directory."); + } catch (err) { + logger.log("error", "Failed to clean temporary directory: " + err); + } + } + } +} + +/** + * Retrieves the current working directory where the script is being executed. This directory is used as a reference + * point for various file operations throughout the build process, ensuring that paths are resolved correctly regardless + * of the location from which the script is invoked. + * + * @returns {string} The absolute path of the current working directory. + */ +function getCurrentDirectory() { + return dirname(fileURLToPath(import.meta.url)); +} + +/** + * Loads the `.buildignore` file and sets up an ignore handler using the `ignore` module. The `.buildignore` file + * contains a list of patterns describing files and directories that should be ignored during the build process. The + * ignore handler created by this method is used to filter files and directories when copying them to the temporary + * directory, ensuring that only necessary files are included in the final mod package. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to an ignore handler. + */ +async function loadBuildIgnoreFile(currentDir) { + const buildIgnorePath = path.join(currentDir, ".buildignore"); + + try { + // Attempt to read the contents of the .buildignore file asynchronously. + const fileContent = await fs.promises.readFile(buildIgnorePath, "utf-8"); + + // Return a new ignore instance and add the rules from the .buildignore file (split by newlines). + return ignore().add(fileContent.split("\n")); + } catch (err) { + logger.log("warn", "Failed to read .buildignore file. No files or directories will be ignored."); + + // Return an empty ignore instance, ensuring the build process can continue. + return ignore(); + } +} + +/** + * Loads the `package.json` file and returns its content as a JSON object. The `package.json` file contains important + * project details such as the name and version, which are used in later stages of the build process to create a + * descriptive name for the mod package. The method reads the file from the current working directory, ensuring that it + * accurately reflects the current state of the project. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to a JSON object containing the contents of the `package.json`. + */ +async function loadPackageJson(currentDir) { + const packageJsonPath = path.join(currentDir, "package.json"); + + // Read the contents of the package.json file asynchronously as a UTF-8 string. + const packageJsonContent = await fs.promises.readFile(packageJsonPath, "utf-8"); + + return JSON.parse(packageJsonContent); +} + +/** + * Constructs a descriptive name for the mod package using details from the `package.json` file. The name is created by + * concatenating the project name, version, and a timestamp, resulting in a unique and descriptive file name for each + * build. This name is used as the base name for the temporary working directory and the final ZIP archive, helping to + * identify different versions of the mod package easily. + * + * @param {Object} packageJson - A JSON object containing the contents of the `package.json` file. + * @returns {string} A string representing the constructed project name. + */ +function createProjectName(packageJson) { + // Remove any non-alphanumeric characters from the author and name. + const author = packageJson.author.replace(/\W/g, ""); + const name = packageJson.name.replace(/\W/g, ""); + const version = packageJson.version; + + // Ensure the name is lowercase, as per the package.json specification. + return `${author}-${name}-${version}`.toLowerCase(); +} + +/** + * Defines the location of the distribution directory where the final mod package will be stored and deletes any + * existing distribution directory to ensure a clean slate for the build process. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to the absolute path to the distribution directory. + */ +async function removeOldDistDirectory(projectDir) { + const distPath = path.join(projectDir, "dist"); + await fs.remove(distPath); + return distPath; +} + +/** + * Creates a temporary working directory using the project name. This directory serves as a staging area where project + * files are gathered before being archived into the final mod package. The method constructs a unique directory path + * by appending the project name to a base temporary directory path, ensuring that each build has its own isolated + * working space. This approach facilitates clean and organized build processes, avoiding potential conflicts with other + * builds. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @param {string} projectName - The constructed project name, used to create a unique path for the temporary directory. + * @returns {Promise} A promise that resolves to the absolute path of the newly created temporary directory. + */ +async function createTemporaryDirectoryWithProjectName(projectName) { + // Create a new directory in the system's temporary folder to hold the project files. + const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "spt-mod-build-")); + + // Create a subdirectory within the temporary directory using the project name for this specific build. + const projectDir = path.join(tempDir, projectName); + await fs.ensureDir(projectDir); + + return projectDir; +} + +/** + * Copies the project files to the temporary directory while respecting the rules defined in the `.buildignore` file. + * The method is recursive, iterating over all files and directories in the source directory and using the ignore + * handler to filter out files and directories that match the patterns defined in the `.buildignore` file. This ensures + * that only the necessary files are included in the final mod package, adhering to the specifications defined by the + * developer in the `.buildignore` file. + * + * The copy operations are delayed and executed in parallel to improve efficiency and reduce the build time. This is + * achieved by creating an array of copy promises and awaiting them all at the end of the function. + * + * @param {string} sourceDirectory - The absolute path of the current working directory. + * @param {string} destinationDirectory - The absolute path of the temporary directory where the files will be copied. + * @param {Ignore} ignoreHandler - The ignore handler created from the `.buildignore` file. + * @returns {Promise} A promise that resolves when all copy operations are completed successfully. + */ +async function copyFiles(srcDir, destDir, ignoreHandler) { + try { + // Read the contents of the source directory to get a list of entries (files and directories). + const entries = await fs.promises.readdir(srcDir, { withFileTypes: true }); + + // Initialize an array to hold the promises returned by recursive calls to copyFiles and copyFile operations. + const copyOperations = []; + + for (const entry of entries) { + // Define the source and destination paths for each entry. + const srcPath = path.join(srcDir, entry.name); + const destPath = path.join(destDir, entry.name); + + // Get the relative path of the source file to check against the ignore handler. + const relativePath = path.relative(process.cwd(), srcPath); + + // If the ignore handler dictates that this file should be ignored, skip to the next iteration. + if (ignoreHandler.ignores(relativePath)) { + logger.log("info", `Ignored: /${path.relative(process.cwd(), srcPath)}`); + continue; + } + + if (entry.isDirectory()) { + // If the entry is a directory, create the corresponding temporary directory and make a recursive call + // to copyFiles to handle copying the contents of the directory. + await fs.ensureDir(destPath); + copyOperations.push(copyFiles(srcPath, destPath, ignoreHandler)); + } else { + // If the entry is a file, add a copyFile operation to the copyOperations array and log the event when + // the operation is successful. + copyOperations.push( + fs.copy(srcPath, destPath).then(() => { + logger.log("info", `Copied: /${path.relative(process.cwd(), srcPath)}`); + }) + ); + } + } + + // Await all copy operations to ensure all files and directories are copied before exiting the function. + await Promise.all(copyOperations); + } catch (err) { + // Log an error message if any error occurs during the copy process. + logger.log("error", "Error copying files: " + err); + } +} + +/** + * Creates a ZIP archive of the project files located in the temporary directory. The method uses the `archiver` module + * to create a ZIP file, which includes all the files that have been copied to the temporary directory during the build + * process. The ZIP file is named using the project name, helping to identify the contents of the archive easily. + * + * @param {string} directoryPath - The absolute path of the temporary directory containing the project files. + * @param {string} projectName - The constructed project name, used to name the ZIP file. + * @returns {Promise} A promise that resolves to the absolute path of the created ZIP file. + */ +async function createZipFile(directoryToZip, zipFilePath, containerDirName) { + return new Promise((resolve, reject) => { + // Create a write stream to the specified ZIP file path. + const output = fs.createWriteStream(zipFilePath); + + // Create a new archiver instance with ZIP format and maximum compression level. + const archive = archiver("zip", { + zlib: { level: 9 }, + }); + + // Set up an event listener for the 'close' event to resolve the promise when the archiver has finalized. + output.on("close", function () { + logger.log("info", "Archiver has finalized. The output and the file descriptor have closed."); + resolve(); + }); + + // Set up an event listener for the 'warning' event to handle warnings appropriately, logging them or rejecting + // the promise based on the error code. + archive.on("warning", function (err) { + if (err.code === "ENOENT") { + logger.log("warn", `Archiver issued a warning: ${err.code} - ${err.message}`); + } else { + reject(err); + } + }); + + // Set up an event listener for the 'error' event to reject the promise if any error occurs during archiving. + archive.on("error", function (err) { + reject(err); + }); + + // Pipe archive data to the file. + archive.pipe(output); + + // Add the directory to the archive, under the provided directory name. + archive.directory(directoryToZip, containerDirName); + + // Finalize the archive, indicating that no more files will be added and triggering the 'close' event once all + // data has been written. + archive.finalize(); + }); +} + +// Engage! +main(); diff --git a/TypeScript/7OnLoadHook/package.json b/TypeScript/7OnLoadHook/package.json index afeb108..f4f0b05 100644 --- a/TypeScript/7OnLoadHook/package.json +++ b/TypeScript/7OnLoadHook/package.json @@ -7,17 +7,20 @@ "akiVersion": "~3.7", "scripts": { "setup": "npm i", - "build": "node ./packageBuild.ts" + "build": "node ./build.mjs", + "buildinfo": "node ./build.mjs --verbose" }, "devDependencies": { "@types/node": "20.4.5", "@typescript-eslint/eslint-plugin": "6.2.0", "@typescript-eslint/parser": "6.2.0", - "bestzip": "2.2.1", + "archiver": "^6.0", "eslint": "8.46.0", - "fs-extra": "11.1.1", - "glob": "10.3.3", + "fs-extra": "^11.1", + "ignore": "^5.2", + "os": "^0.1", "tsyringe": "4.8.0", - "typescript": "5.1.6" + "typescript": "5.1.6", + "winston": "^3.9" } -} \ No newline at end of file +} diff --git a/TypeScript/7OnLoadHook/packageBuild.ts b/TypeScript/7OnLoadHook/packageBuild.ts deleted file mode 100644 index d76ddd8..0000000 --- a/TypeScript/7OnLoadHook/packageBuild.ts +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env node - -// This is a simple script used to build a mod package. The script will copy necessary files to the build directory -// and compress the build directory into a zip file that can be easily shared. - -const fs = require("fs-extra"); -const glob = require("glob"); -const zip = require('bestzip'); -const path = require("path"); -const minimatch = require('minimatch'); - -// Load the package.json file to get some information about the package so we can name things appropriately. This is -// atypical, and you would never do this in a production environment, but this script is only used for development so -// it's fine in this case. Some of these values are stored in environment variables, but those differ between node -// versions; the 'author' value is not available after node v14. -const { author, name:packageName, version } = require("./package.json"); - -// Generate the name of the package, stripping out all non-alphanumeric characters in the 'author' and 'name'. -const modName = `${author.replace(/[^a-z0-9]/gi, "")}-${packageName.replace(/[^a-z0-9]/gi, "")}-${version}`; -console.log(`Generated package name: ${modName}`); - -// Delete the old build directory and compressed package file. -fs.rmSync(`${__dirname}/dist`, { force: true, recursive: true }); -console.log("Previous build files deleted."); - -// Generate a list of files that should not be copied over into the distribution directory. This is a blacklist to ensure -// we always copy over additional files and directories that authors may have added to their project. This may need to be -// expanded upon by the mod author to allow for node modules that are used within the mod; example commented out below. -const ignoreList = [ - "node_modules/", - // "node_modules/!(weighted|glob)", // Instead of excluding the entire node_modules directory, allow two node modules. - "src/**/*.js", - "types/", - ".git/", - ".gitea/", - ".eslintignore", - ".eslintrc.json", - ".gitignore", - ".DS_Store", - "packageBuild.ts", - "mod.code-workspace", - "package-lock.json", - "tsconfig.json" -]; -const exclude = glob.sync(`{${ignoreList.join(",")}}`, { realpath: true, dot: true }); - -fs.copySync(__dirname, path.normalize(`${__dirname}/../~${modName}`), {filter: (src) => -{ - const relativePath = path.relative(__dirname, src); - const shouldExclude = exclude.some((pattern) => minimatch(relativePath, pattern)); - console.log(`${relativePath} - Excluded: ${shouldExclude}`); - return !shouldExclude; -},}); - -fs.moveSync(path.normalize(`${__dirname}/../~${modName}`), path.normalize(`${__dirname}/${modName}`), { overwrite: true }); -fs.copySync(path.normalize(`${__dirname}/${modName}`), path.normalize(`${__dirname}/dist`)); -console.log("Build files copied."); - -// Compress the files for easy distribution. The compressed file is saved into the dist directory. When uncompressed we -// need to be sure that it includes a directory that the user can easily copy into their game mods directory. -zip({ - source: modName, - destination: `dist/${modName}.zip`, - cwd: __dirname -}).catch(function(err) -{ - console.error("A bestzip error has occurred: ", err.stack); -}).then(function() -{ - console.log(`Compressed mod package to: /dist/${modName}.zip`); - - // Now that we're done with the compression we can delete the temporary build directory. - fs.rmSync(`${__dirname}/${modName}`, { force: true, recursive: true }); - console.log("Build successful! your zip file has been created and is ready to be uploaded to hub.sp-tarkov.com/files/"); -}); \ No newline at end of file diff --git a/TypeScript/8OnUpdateHook/.buildignore b/TypeScript/8OnUpdateHook/.buildignore new file mode 100644 index 0000000..2cbde65 --- /dev/null +++ b/TypeScript/8OnUpdateHook/.buildignore @@ -0,0 +1,20 @@ +/.buildignore +/.DS_Store +/.editorconfig +/.eslintignore +/.eslintrc.json +/.git +/.github +/.gitignore +/.gitlab +/.nvmrc +/.prettierrc +/.vscode +/build.mjs +/dist +/images +/mod.code-workspace +/node_modules +/package-lock.json +/tsconfig.json +/types diff --git a/TypeScript/8OnUpdateHook/.eslintrc.json b/TypeScript/8OnUpdateHook/.eslintrc.json index c505160..071a313 100644 --- a/TypeScript/8OnUpdateHook/.eslintrc.json +++ b/TypeScript/8OnUpdateHook/.eslintrc.json @@ -1,75 +1,98 @@ { - "root": true, - "parser": "@typescript-eslint/parser", - "plugins": [ - "@typescript-eslint" - ], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended" - ], - "rules": { - "@typescript-eslint/no-explicit-any": 0, - "@typescript-eslint/no-unused-vars": 1, - "@typescript-eslint/no-empty-interface": 0, - "@typescript-eslint/no-namespace": 0, - "@typescript-eslint/comma-dangle": 1, - "@typescript-eslint/func-call-spacing": 2, - "@typescript-eslint/quotes": 1, - "@typescript-eslint/brace-style": [ - "warn", - "allman" + "root": true, + "parser": "@typescript-eslint/parser", + "plugins": [ + "@typescript-eslint" ], - "@typescript-eslint/naming-convention": [ - "warn", - { - "selector": "default", - "format": ["camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "typeLike", - "format": ["PascalCase"] - }, - { - "selector": "objectLiteralProperty", - "format": ["PascalCase", "camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "typeProperty", - "format": ["PascalCase", "camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "enumMember", - "format": ["UPPER_CASE"] - } + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" ], - "@typescript-eslint/indent": [ - "warn", - 4 - ], - "@typescript-eslint/no-unused-expressions": [ - "warn", - { - "allowShortCircuit": false, - "allowTernary": false - } - ], - "@typescript-eslint/keyword-spacing": [ - "warn", - { - "before": true, - "after": true - } - ], - "@typescript-eslint/explicit-module-boundary-types": [ - "warn", - { - "allowArgumentsExplicitlyTypedAsAny": true - } + "rules": { + "@typescript-eslint/no-explicit-any": 0, + "@typescript-eslint/no-unused-vars": 1, + "@typescript-eslint/no-empty-interface": 0, + "@typescript-eslint/no-namespace": 0, + "@typescript-eslint/comma-dangle": 1, + "@typescript-eslint/func-call-spacing": 2, + "@typescript-eslint/quotes": 1, + "@typescript-eslint/brace-style": [ + "warn", + "allman" + ], + "@typescript-eslint/naming-convention": [ + "warn", + { + "selector": "default", + "format": [ + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "typeLike", + "format": [ + "PascalCase" + ] + }, + { + "selector": "objectLiteralProperty", + "format": [ + "PascalCase", + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "typeProperty", + "format": [ + "PascalCase", + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "enumMember", + "format": [ + "UPPER_CASE" + ] + } + ], + "@typescript-eslint/indent": [ + "warn", + 4 + ], + "@typescript-eslint/no-unused-expressions": [ + "warn", + { + "allowShortCircuit": false, + "allowTernary": false + } + ], + "@typescript-eslint/keyword-spacing": [ + "warn", + { + "before": true, + "after": true + } + ], + "@typescript-eslint/explicit-module-boundary-types": [ + "warn", + { + "allowArgumentsExplicitlyTypedAsAny": true + } + ] + }, + "overrides": [ + { + "files": [ + "*.mjs", + "*.ts" + ], + "env": { + "node": true + } + } ] - } } \ No newline at end of file diff --git a/TypeScript/8OnUpdateHook/README.md b/TypeScript/8OnUpdateHook/README.md index 434d063..8cba28e 100644 --- a/TypeScript/8OnUpdateHook/README.md +++ b/TypeScript/8OnUpdateHook/README.md @@ -1,64 +1,66 @@ +# Welcome to the SPT-AKI Modding Project -This project was created to automate most parts of building and setting up an environment. +This project is designed to streamline the initial setup process for building and creating mods in the SPT-AKI environment. Follow this guide to set up your environment efficiently. -## **NodeJS:** +## **Table of Contents** +- [NodeJS Setup](#nodejs-setup) +- [IDE Setup](#ide-setup) +- [Workspace Configuration](#workspace-configuration) +- [Environment Setup](#environment-setup) +- [Essential Concepts](#essential-concepts) +- [Coding Guidelines](#coding-guidelines) +- [Distribution Guidelines](#distribution-guidelines) -The first step would be to install nodejs on your pc, the version you NEED is **16.17.1** +## **NodeJS Setup** -That version is the one that has been used to test the mod templates and build scripts. +Before you begin, ensure to install NodeJS version `v16.17.1`, which has been tested thoroughly with our mod templates and build scripts. Download it from the [official NodeJS website](https://nodejs.org/). -It can be downloaded from here: https://nodejs.org/dist/v16.17.1/node-v16.17.1-x64.msi +After installation, it's advised to reboot your system. -A system reboot may be needed after install. +## **IDE Setup** -## **IDE:** +For this project, you can work with either [VSCodium](https://vscodium.com/) or [VSCode](https://code.visualstudio.com/). However, we strongly recommend using VSCode, as all development and testing have been carried out using this IDE, ensuring a smoother experience and compatibility with the project setups. Either way, we have a prepared a workspace file to assist you in setting up your environment. -The second step is having an IDE ready. We've setup a VSCodium workspace file to help with this. +## **Workspace Configuration** -You CAN use Visual Studio Code if you so desire, just keep in mind that our devs tests on the mod files was done using VSCode. +With NodeJS and your chosen IDE ready, initiate the `mod.code-workspace` file using your IDE: -You can get VSCode here: https://code.visualstudio.com/ +> File -> Open Workspace from File... -## **Workspace:** +Upon project loading, consider installing recommended plugins like the ESLint plugin. -Once you have NodeJS and VSCode/VSCodium ready, open the mod.code-workspace file with VSCode (File->Open Workspace from File...). +## **Environment Setup** -Once the project loads you may be recommended to install the ESLint plugin. This is HIGHLY recommended. +An automated task is available to configure your environment for Typescript utilization: -## **Environment Setup:** +> Terminal -> Run Task... -> Show All Tasks... -> npm: install -There is a task that will automatically setup your environment to use typescript. +Note: Preserve the `node_modules` folder as it contains necessary dependencies for Typescript and other functionalities. -To run it, you just need to go to: +## **Essential Concepts** -> Terminal->Run Task...->Show All Tasks...->npm: install +Prioritize understanding Dependency Injection and Inversion of Control, the architectural principles SPT-AKI adopts. Comprehensive guidelines will be available on the hub upon release. -After running this task, your environment will be ready to start coding. +Some resources to get you started: + - [A quick intro to Dependency Injection](https://www.freecodecamp.org/news/a-quick-intro-to-dependency-injection-what-it-is-and-when-to-use-it-7578c84fa88f/) + - [Understanding Inversion of Control (IoC) Principle](https://medium.com/@amitkma/understanding-inversion-of-control-ioc-principle-163b1dc97454) -DO NOT remove the node_modules folder, this is an auto generated directory that has the required dependencies to be able to use typescript and more. +## **Coding Guidelines** -## **IMPORTANT:** +Focus your mod development around the `mod.ts` file. In the `package.json` file, only alter these properties: `"name"`, `"version"`, `"license"`, `"author"`, and `"akiVersion"`. -Before starting to work on your mod, we suggest you read about Dependency Injection and Inversion of Control as this is the adopted architecture SPT-AKI has adopted. +New to Typescript? Find comprehensive documentation on the [official website](https://www.typescriptlang.org/docs/). -It will be difficult to understand some of the problems you may be having if you dont understand the basics of it. +## **Distribution Guidelines** -A guide explaining all the essentials will be available on the hub on release for you to read about. +Automated tasks are set up to bundle all necessary files for your mod to function in SPT-AKI: -## **Coding:** +> Terminal -> Run Task... -> Show All Tasks... -> npm: build -All your work should be centered around the mod.ts file as an entry point. -You can ONLY change the following properties from the package.json file: `"name"`, `"version"`, `"license"`: `"MIT"`, `"author"`, `"akiVersion"`. +The ZIP output, located in the `dist` directory, contains all required files. Ensure all files are included and modify the `.buildignore` file as needed. This ZIP file is your uploadable asset for the hub. -If you have never used typescript before, you can read about it here: https://www.typescriptlang.org/docs/ +## **Conclusion** -## **Distributing your mod:** +With this setup, you're ready to begin modding with SPT-AKI. If you run into any trouble be sure to check out the [modding documentation on the hub](https://hub.sp-tarkov.com/doc/lexicon/66-modding/). If you really get stuck feel free to join us in the [#mods-development](https://discord.com/channels/875684761291599922/875803116409323562) official Discord channel. -The project has been set up with an automatic task that will copy and zip ALL required files for your mod to work on SPT-AKI. -To run this task you just need to go to: - -> Terminal->Run Task...->Show All Tasks...->npm: build:zip - -The output will be a mod.zip file that will appear on the root of the project. - -Always verify that all files were included into the zip file. \ No newline at end of file +Build something awesome! diff --git a/TypeScript/8OnUpdateHook/build.mjs b/TypeScript/8OnUpdateHook/build.mjs new file mode 100644 index 0000000..d00117c --- /dev/null +++ b/TypeScript/8OnUpdateHook/build.mjs @@ -0,0 +1,383 @@ +#!/usr/bin/env node + +/** + * Build Script + * + * This script automates the build process for server-side SPT mod projects, facilitating the creation of distributable + * mod packages. It performs a series of operations as outlined below: + * - Loads the .buildignore file, which is used to list files that should be ignored during the build process. + * - Loads the package.json to get project details so a descriptive name can be created for the mod package. + * - Creates a distribution directory and a temporary working directory. + * - Copies files to the temporary directory while respecting the .buildignore rules. + * - Creates a zip archive of the project files. + * - Moves the zip file to the root of the distribution directory. + * - Cleans up the temporary directory. + * + * It's typical that this script be customized to suit the needs of each project. For example, the script can be updated + * to perform additional operations, such as moving the mod package to a specific location or uploading it to a server. + * This script is intended to be a starting point for developers to build upon. + * + * Usage: + * - Run this script using npm: `npm run build` + * - Use `npm run buildinfo` for detailed logging. + * + * Note: + * - Ensure that all necessary Node.js modules are installed before running the script: `npm install` + * - The script reads configurations from the `package.json` and `.buildignore` files; ensure they are correctly set up. + * + * @author Refringe + * @version v1.0.0 + */ + +import fs from "fs-extra"; +import os from "os"; +import path from "path"; +import { fileURLToPath } from "url"; +import { dirname } from "path"; +import ignore from "ignore"; +import archiver from "archiver"; +import winston from "winston"; + +// Get the command line arguments to determine whether to use verbose logging. +const args = process.argv.slice(2); +const verbose = args.includes("--verbose") || args.includes("-v"); + +// Configure the Winston logger to use colours. +const logColors = { + error: "red", + warn: "yellow", + info: "grey", + success: "green", +}; +winston.addColors(logColors); + +// Create a logger instance to log build progress. Configure the logger levels to allow for different levels of logging +// based on the verbosity flag, and set the console transport to log messages of the appropriate level. +const logger = winston.createLogger({ + levels: { + error: 0, + warn: 1, + success: 2, + info: 3, + }, + format: winston.format.combine( + winston.format.colorize(), + winston.format.printf(info => { + return `${info.level}: ${info.message}`; + }) + ), + transports: [ + new winston.transports.Console({ + level: verbose ? "info" : "success", + }), + ], +}); + +/** + * The main function orchestrates the build process for creating a distributable mod package. It leverages a series of + * helper functions to perform various tasks such as loading configuration files, setting up directories, copying files + * according to `.buildignore` rules, and creating a ZIP archive of the project files. + * + * Utilizes the Winston logger to provide information on the build status at different stages of the process. + * + * @returns {void} + */ +async function main() { + // Get the current directory where the script is being executed + const currentDir = getCurrentDirectory(); + + // Defining at this scope because we need to use it in the finally block. + let projectDir; + + try { + // Load the .buildignore file to set up an ignore handler for the build process. + const buildIgnorePatterns = await loadBuildIgnoreFile(currentDir); + + // Load the package.json file to get project details. + const packageJson = await loadPackageJson(currentDir); + + // Create a descriptive name for the mod package. + const projectName = createProjectName(packageJson); + logger.log("success", `Project name created: ${projectName}`); + + // Remove the old distribution directory and create a fresh one. + const distDir = await removeOldDistDirectory(currentDir); + logger.log("info", "Distribution directory successfully cleaned."); + + // Create a temporary working directory to perform the build operations. + projectDir = await createTemporaryDirectoryWithProjectName(projectName); + logger.log("success", "Temporary working directory successfully created."); + logger.log("info", projectDir); + + // Copy files to the temporary directory while respecting the .buildignore rules. + logger.log("info", "Beginning copy operation using .buildignore file..."); + await copyFiles(currentDir, projectDir, buildIgnorePatterns); + logger.log("success", "Files successfully copied to temporary directory."); + + // Create a zip archive of the project files. + logger.log("info", "Beginning folder compression..."); + const zipFilePath = path.join(path.dirname(projectDir), `${projectName}.zip`); + await createZipFile(projectDir, zipFilePath, "user/mods/" + projectName); + logger.log("success", "Archive successfully created."); + logger.log("info", zipFilePath); + + // Move the zip file inside of the project directory, within the temporary working directory. + const zipFileInProjectDir = path.join(projectDir, `${projectName}.zip`); + await fs.move(zipFilePath, zipFileInProjectDir); + logger.log("success", "Archive successfully moved."); + logger.log("info", zipFileInProjectDir); + + // Move the temporary directory into the distribution directory. + await fs.move(projectDir, distDir); + logger.log("success", "Temporary directory successfully moved into project distribution directory."); + + // Log the success message. Write out the path to the mod package. + logger.log("success", "------------------------------------"); + logger.log("success", "Build script completed successfully!"); + logger.log("success", "Your mod package has been created in the 'dist' directory:"); + logger.log("success", `/${path.relative(process.cwd(), path.join(distDir, `${projectName}.zip`))}`); + logger.log("success", "------------------------------------"); + if (!verbose) { + logger.log("success", "To see a detailed build log, use `npm run buildinfo`."); + logger.log("success", "------------------------------------"); + } + } catch (err) { + // If any of the file operations fail, log the error. + logger.log("error", "An error occurred: " + err); + } finally { + // Clean up the temporary directory, even if the build fails. + if (projectDir) { + try { + await fs.promises.rm(projectDir, { force: true, recursive: true }); + logger.log("info", "Cleaned temporary directory."); + } catch (err) { + logger.log("error", "Failed to clean temporary directory: " + err); + } + } + } +} + +/** + * Retrieves the current working directory where the script is being executed. This directory is used as a reference + * point for various file operations throughout the build process, ensuring that paths are resolved correctly regardless + * of the location from which the script is invoked. + * + * @returns {string} The absolute path of the current working directory. + */ +function getCurrentDirectory() { + return dirname(fileURLToPath(import.meta.url)); +} + +/** + * Loads the `.buildignore` file and sets up an ignore handler using the `ignore` module. The `.buildignore` file + * contains a list of patterns describing files and directories that should be ignored during the build process. The + * ignore handler created by this method is used to filter files and directories when copying them to the temporary + * directory, ensuring that only necessary files are included in the final mod package. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to an ignore handler. + */ +async function loadBuildIgnoreFile(currentDir) { + const buildIgnorePath = path.join(currentDir, ".buildignore"); + + try { + // Attempt to read the contents of the .buildignore file asynchronously. + const fileContent = await fs.promises.readFile(buildIgnorePath, "utf-8"); + + // Return a new ignore instance and add the rules from the .buildignore file (split by newlines). + return ignore().add(fileContent.split("\n")); + } catch (err) { + logger.log("warn", "Failed to read .buildignore file. No files or directories will be ignored."); + + // Return an empty ignore instance, ensuring the build process can continue. + return ignore(); + } +} + +/** + * Loads the `package.json` file and returns its content as a JSON object. The `package.json` file contains important + * project details such as the name and version, which are used in later stages of the build process to create a + * descriptive name for the mod package. The method reads the file from the current working directory, ensuring that it + * accurately reflects the current state of the project. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to a JSON object containing the contents of the `package.json`. + */ +async function loadPackageJson(currentDir) { + const packageJsonPath = path.join(currentDir, "package.json"); + + // Read the contents of the package.json file asynchronously as a UTF-8 string. + const packageJsonContent = await fs.promises.readFile(packageJsonPath, "utf-8"); + + return JSON.parse(packageJsonContent); +} + +/** + * Constructs a descriptive name for the mod package using details from the `package.json` file. The name is created by + * concatenating the project name, version, and a timestamp, resulting in a unique and descriptive file name for each + * build. This name is used as the base name for the temporary working directory and the final ZIP archive, helping to + * identify different versions of the mod package easily. + * + * @param {Object} packageJson - A JSON object containing the contents of the `package.json` file. + * @returns {string} A string representing the constructed project name. + */ +function createProjectName(packageJson) { + // Remove any non-alphanumeric characters from the author and name. + const author = packageJson.author.replace(/\W/g, ""); + const name = packageJson.name.replace(/\W/g, ""); + const version = packageJson.version; + + // Ensure the name is lowercase, as per the package.json specification. + return `${author}-${name}-${version}`.toLowerCase(); +} + +/** + * Defines the location of the distribution directory where the final mod package will be stored and deletes any + * existing distribution directory to ensure a clean slate for the build process. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to the absolute path to the distribution directory. + */ +async function removeOldDistDirectory(projectDir) { + const distPath = path.join(projectDir, "dist"); + await fs.remove(distPath); + return distPath; +} + +/** + * Creates a temporary working directory using the project name. This directory serves as a staging area where project + * files are gathered before being archived into the final mod package. The method constructs a unique directory path + * by appending the project name to a base temporary directory path, ensuring that each build has its own isolated + * working space. This approach facilitates clean and organized build processes, avoiding potential conflicts with other + * builds. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @param {string} projectName - The constructed project name, used to create a unique path for the temporary directory. + * @returns {Promise} A promise that resolves to the absolute path of the newly created temporary directory. + */ +async function createTemporaryDirectoryWithProjectName(projectName) { + // Create a new directory in the system's temporary folder to hold the project files. + const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "spt-mod-build-")); + + // Create a subdirectory within the temporary directory using the project name for this specific build. + const projectDir = path.join(tempDir, projectName); + await fs.ensureDir(projectDir); + + return projectDir; +} + +/** + * Copies the project files to the temporary directory while respecting the rules defined in the `.buildignore` file. + * The method is recursive, iterating over all files and directories in the source directory and using the ignore + * handler to filter out files and directories that match the patterns defined in the `.buildignore` file. This ensures + * that only the necessary files are included in the final mod package, adhering to the specifications defined by the + * developer in the `.buildignore` file. + * + * The copy operations are delayed and executed in parallel to improve efficiency and reduce the build time. This is + * achieved by creating an array of copy promises and awaiting them all at the end of the function. + * + * @param {string} sourceDirectory - The absolute path of the current working directory. + * @param {string} destinationDirectory - The absolute path of the temporary directory where the files will be copied. + * @param {Ignore} ignoreHandler - The ignore handler created from the `.buildignore` file. + * @returns {Promise} A promise that resolves when all copy operations are completed successfully. + */ +async function copyFiles(srcDir, destDir, ignoreHandler) { + try { + // Read the contents of the source directory to get a list of entries (files and directories). + const entries = await fs.promises.readdir(srcDir, { withFileTypes: true }); + + // Initialize an array to hold the promises returned by recursive calls to copyFiles and copyFile operations. + const copyOperations = []; + + for (const entry of entries) { + // Define the source and destination paths for each entry. + const srcPath = path.join(srcDir, entry.name); + const destPath = path.join(destDir, entry.name); + + // Get the relative path of the source file to check against the ignore handler. + const relativePath = path.relative(process.cwd(), srcPath); + + // If the ignore handler dictates that this file should be ignored, skip to the next iteration. + if (ignoreHandler.ignores(relativePath)) { + logger.log("info", `Ignored: /${path.relative(process.cwd(), srcPath)}`); + continue; + } + + if (entry.isDirectory()) { + // If the entry is a directory, create the corresponding temporary directory and make a recursive call + // to copyFiles to handle copying the contents of the directory. + await fs.ensureDir(destPath); + copyOperations.push(copyFiles(srcPath, destPath, ignoreHandler)); + } else { + // If the entry is a file, add a copyFile operation to the copyOperations array and log the event when + // the operation is successful. + copyOperations.push( + fs.copy(srcPath, destPath).then(() => { + logger.log("info", `Copied: /${path.relative(process.cwd(), srcPath)}`); + }) + ); + } + } + + // Await all copy operations to ensure all files and directories are copied before exiting the function. + await Promise.all(copyOperations); + } catch (err) { + // Log an error message if any error occurs during the copy process. + logger.log("error", "Error copying files: " + err); + } +} + +/** + * Creates a ZIP archive of the project files located in the temporary directory. The method uses the `archiver` module + * to create a ZIP file, which includes all the files that have been copied to the temporary directory during the build + * process. The ZIP file is named using the project name, helping to identify the contents of the archive easily. + * + * @param {string} directoryPath - The absolute path of the temporary directory containing the project files. + * @param {string} projectName - The constructed project name, used to name the ZIP file. + * @returns {Promise} A promise that resolves to the absolute path of the created ZIP file. + */ +async function createZipFile(directoryToZip, zipFilePath, containerDirName) { + return new Promise((resolve, reject) => { + // Create a write stream to the specified ZIP file path. + const output = fs.createWriteStream(zipFilePath); + + // Create a new archiver instance with ZIP format and maximum compression level. + const archive = archiver("zip", { + zlib: { level: 9 }, + }); + + // Set up an event listener for the 'close' event to resolve the promise when the archiver has finalized. + output.on("close", function () { + logger.log("info", "Archiver has finalized. The output and the file descriptor have closed."); + resolve(); + }); + + // Set up an event listener for the 'warning' event to handle warnings appropriately, logging them or rejecting + // the promise based on the error code. + archive.on("warning", function (err) { + if (err.code === "ENOENT") { + logger.log("warn", `Archiver issued a warning: ${err.code} - ${err.message}`); + } else { + reject(err); + } + }); + + // Set up an event listener for the 'error' event to reject the promise if any error occurs during archiving. + archive.on("error", function (err) { + reject(err); + }); + + // Pipe archive data to the file. + archive.pipe(output); + + // Add the directory to the archive, under the provided directory name. + archive.directory(directoryToZip, containerDirName); + + // Finalize the archive, indicating that no more files will be added and triggering the 'close' event once all + // data has been written. + archive.finalize(); + }); +} + +// Engage! +main(); diff --git a/TypeScript/8OnUpdateHook/package.json b/TypeScript/8OnUpdateHook/package.json index 30421ee..032cf4c 100644 --- a/TypeScript/8OnUpdateHook/package.json +++ b/TypeScript/8OnUpdateHook/package.json @@ -7,17 +7,20 @@ "akiVersion": "~3.7", "scripts": { "setup": "npm i", - "build": "node ./packageBuild.ts" + "build": "node ./build.mjs", + "buildinfo": "node ./build.mjs --verbose" }, "devDependencies": { "@types/node": "20.4.5", "@typescript-eslint/eslint-plugin": "6.2.0", "@typescript-eslint/parser": "6.2.0", - "bestzip": "2.2.1", + "archiver": "^6.0", "eslint": "8.46.0", - "fs-extra": "11.1.1", - "glob": "10.3.3", + "fs-extra": "^11.1", + "ignore": "^5.2", + "os": "^0.1", "tsyringe": "4.8.0", - "typescript": "5.1.6" + "typescript": "5.1.6", + "winston": "^3.9" } -} \ No newline at end of file +} diff --git a/TypeScript/8OnUpdateHook/packageBuild.ts b/TypeScript/8OnUpdateHook/packageBuild.ts deleted file mode 100644 index d76ddd8..0000000 --- a/TypeScript/8OnUpdateHook/packageBuild.ts +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env node - -// This is a simple script used to build a mod package. The script will copy necessary files to the build directory -// and compress the build directory into a zip file that can be easily shared. - -const fs = require("fs-extra"); -const glob = require("glob"); -const zip = require('bestzip'); -const path = require("path"); -const minimatch = require('minimatch'); - -// Load the package.json file to get some information about the package so we can name things appropriately. This is -// atypical, and you would never do this in a production environment, but this script is only used for development so -// it's fine in this case. Some of these values are stored in environment variables, but those differ between node -// versions; the 'author' value is not available after node v14. -const { author, name:packageName, version } = require("./package.json"); - -// Generate the name of the package, stripping out all non-alphanumeric characters in the 'author' and 'name'. -const modName = `${author.replace(/[^a-z0-9]/gi, "")}-${packageName.replace(/[^a-z0-9]/gi, "")}-${version}`; -console.log(`Generated package name: ${modName}`); - -// Delete the old build directory and compressed package file. -fs.rmSync(`${__dirname}/dist`, { force: true, recursive: true }); -console.log("Previous build files deleted."); - -// Generate a list of files that should not be copied over into the distribution directory. This is a blacklist to ensure -// we always copy over additional files and directories that authors may have added to their project. This may need to be -// expanded upon by the mod author to allow for node modules that are used within the mod; example commented out below. -const ignoreList = [ - "node_modules/", - // "node_modules/!(weighted|glob)", // Instead of excluding the entire node_modules directory, allow two node modules. - "src/**/*.js", - "types/", - ".git/", - ".gitea/", - ".eslintignore", - ".eslintrc.json", - ".gitignore", - ".DS_Store", - "packageBuild.ts", - "mod.code-workspace", - "package-lock.json", - "tsconfig.json" -]; -const exclude = glob.sync(`{${ignoreList.join(",")}}`, { realpath: true, dot: true }); - -fs.copySync(__dirname, path.normalize(`${__dirname}/../~${modName}`), {filter: (src) => -{ - const relativePath = path.relative(__dirname, src); - const shouldExclude = exclude.some((pattern) => minimatch(relativePath, pattern)); - console.log(`${relativePath} - Excluded: ${shouldExclude}`); - return !shouldExclude; -},}); - -fs.moveSync(path.normalize(`${__dirname}/../~${modName}`), path.normalize(`${__dirname}/${modName}`), { overwrite: true }); -fs.copySync(path.normalize(`${__dirname}/${modName}`), path.normalize(`${__dirname}/dist`)); -console.log("Build files copied."); - -// Compress the files for easy distribution. The compressed file is saved into the dist directory. When uncompressed we -// need to be sure that it includes a directory that the user can easily copy into their game mods directory. -zip({ - source: modName, - destination: `dist/${modName}.zip`, - cwd: __dirname -}).catch(function(err) -{ - console.error("A bestzip error has occurred: ", err.stack); -}).then(function() -{ - console.log(`Compressed mod package to: /dist/${modName}.zip`); - - // Now that we're done with the compression we can delete the temporary build directory. - fs.rmSync(`${__dirname}/${modName}`, { force: true, recursive: true }); - console.log("Build successful! your zip file has been created and is ready to be uploaded to hub.sp-tarkov.com/files/"); -}); \ No newline at end of file diff --git a/TypeScript/9RouterHooks/.buildignore b/TypeScript/9RouterHooks/.buildignore new file mode 100644 index 0000000..2cbde65 --- /dev/null +++ b/TypeScript/9RouterHooks/.buildignore @@ -0,0 +1,20 @@ +/.buildignore +/.DS_Store +/.editorconfig +/.eslintignore +/.eslintrc.json +/.git +/.github +/.gitignore +/.gitlab +/.nvmrc +/.prettierrc +/.vscode +/build.mjs +/dist +/images +/mod.code-workspace +/node_modules +/package-lock.json +/tsconfig.json +/types diff --git a/TypeScript/9RouterHooks/.eslintrc.json b/TypeScript/9RouterHooks/.eslintrc.json index c505160..071a313 100644 --- a/TypeScript/9RouterHooks/.eslintrc.json +++ b/TypeScript/9RouterHooks/.eslintrc.json @@ -1,75 +1,98 @@ { - "root": true, - "parser": "@typescript-eslint/parser", - "plugins": [ - "@typescript-eslint" - ], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended" - ], - "rules": { - "@typescript-eslint/no-explicit-any": 0, - "@typescript-eslint/no-unused-vars": 1, - "@typescript-eslint/no-empty-interface": 0, - "@typescript-eslint/no-namespace": 0, - "@typescript-eslint/comma-dangle": 1, - "@typescript-eslint/func-call-spacing": 2, - "@typescript-eslint/quotes": 1, - "@typescript-eslint/brace-style": [ - "warn", - "allman" + "root": true, + "parser": "@typescript-eslint/parser", + "plugins": [ + "@typescript-eslint" ], - "@typescript-eslint/naming-convention": [ - "warn", - { - "selector": "default", - "format": ["camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "typeLike", - "format": ["PascalCase"] - }, - { - "selector": "objectLiteralProperty", - "format": ["PascalCase", "camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "typeProperty", - "format": ["PascalCase", "camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": "enumMember", - "format": ["UPPER_CASE"] - } + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" ], - "@typescript-eslint/indent": [ - "warn", - 4 - ], - "@typescript-eslint/no-unused-expressions": [ - "warn", - { - "allowShortCircuit": false, - "allowTernary": false - } - ], - "@typescript-eslint/keyword-spacing": [ - "warn", - { - "before": true, - "after": true - } - ], - "@typescript-eslint/explicit-module-boundary-types": [ - "warn", - { - "allowArgumentsExplicitlyTypedAsAny": true - } + "rules": { + "@typescript-eslint/no-explicit-any": 0, + "@typescript-eslint/no-unused-vars": 1, + "@typescript-eslint/no-empty-interface": 0, + "@typescript-eslint/no-namespace": 0, + "@typescript-eslint/comma-dangle": 1, + "@typescript-eslint/func-call-spacing": 2, + "@typescript-eslint/quotes": 1, + "@typescript-eslint/brace-style": [ + "warn", + "allman" + ], + "@typescript-eslint/naming-convention": [ + "warn", + { + "selector": "default", + "format": [ + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "typeLike", + "format": [ + "PascalCase" + ] + }, + { + "selector": "objectLiteralProperty", + "format": [ + "PascalCase", + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "typeProperty", + "format": [ + "PascalCase", + "camelCase" + ], + "leadingUnderscore": "allow" + }, + { + "selector": "enumMember", + "format": [ + "UPPER_CASE" + ] + } + ], + "@typescript-eslint/indent": [ + "warn", + 4 + ], + "@typescript-eslint/no-unused-expressions": [ + "warn", + { + "allowShortCircuit": false, + "allowTernary": false + } + ], + "@typescript-eslint/keyword-spacing": [ + "warn", + { + "before": true, + "after": true + } + ], + "@typescript-eslint/explicit-module-boundary-types": [ + "warn", + { + "allowArgumentsExplicitlyTypedAsAny": true + } + ] + }, + "overrides": [ + { + "files": [ + "*.mjs", + "*.ts" + ], + "env": { + "node": true + } + } ] - } } \ No newline at end of file diff --git a/TypeScript/9RouterHooks/README.md b/TypeScript/9RouterHooks/README.md index 434d063..8cba28e 100644 --- a/TypeScript/9RouterHooks/README.md +++ b/TypeScript/9RouterHooks/README.md @@ -1,64 +1,66 @@ +# Welcome to the SPT-AKI Modding Project -This project was created to automate most parts of building and setting up an environment. +This project is designed to streamline the initial setup process for building and creating mods in the SPT-AKI environment. Follow this guide to set up your environment efficiently. -## **NodeJS:** +## **Table of Contents** +- [NodeJS Setup](#nodejs-setup) +- [IDE Setup](#ide-setup) +- [Workspace Configuration](#workspace-configuration) +- [Environment Setup](#environment-setup) +- [Essential Concepts](#essential-concepts) +- [Coding Guidelines](#coding-guidelines) +- [Distribution Guidelines](#distribution-guidelines) -The first step would be to install nodejs on your pc, the version you NEED is **16.17.1** +## **NodeJS Setup** -That version is the one that has been used to test the mod templates and build scripts. +Before you begin, ensure to install NodeJS version `v16.17.1`, which has been tested thoroughly with our mod templates and build scripts. Download it from the [official NodeJS website](https://nodejs.org/). -It can be downloaded from here: https://nodejs.org/dist/v16.17.1/node-v16.17.1-x64.msi +After installation, it's advised to reboot your system. -A system reboot may be needed after install. +## **IDE Setup** -## **IDE:** +For this project, you can work with either [VSCodium](https://vscodium.com/) or [VSCode](https://code.visualstudio.com/). However, we strongly recommend using VSCode, as all development and testing have been carried out using this IDE, ensuring a smoother experience and compatibility with the project setups. Either way, we have a prepared a workspace file to assist you in setting up your environment. -The second step is having an IDE ready. We've setup a VSCodium workspace file to help with this. +## **Workspace Configuration** -You CAN use Visual Studio Code if you so desire, just keep in mind that our devs tests on the mod files was done using VSCode. +With NodeJS and your chosen IDE ready, initiate the `mod.code-workspace` file using your IDE: -You can get VSCode here: https://code.visualstudio.com/ +> File -> Open Workspace from File... -## **Workspace:** +Upon project loading, consider installing recommended plugins like the ESLint plugin. -Once you have NodeJS and VSCode/VSCodium ready, open the mod.code-workspace file with VSCode (File->Open Workspace from File...). +## **Environment Setup** -Once the project loads you may be recommended to install the ESLint plugin. This is HIGHLY recommended. +An automated task is available to configure your environment for Typescript utilization: -## **Environment Setup:** +> Terminal -> Run Task... -> Show All Tasks... -> npm: install -There is a task that will automatically setup your environment to use typescript. +Note: Preserve the `node_modules` folder as it contains necessary dependencies for Typescript and other functionalities. -To run it, you just need to go to: +## **Essential Concepts** -> Terminal->Run Task...->Show All Tasks...->npm: install +Prioritize understanding Dependency Injection and Inversion of Control, the architectural principles SPT-AKI adopts. Comprehensive guidelines will be available on the hub upon release. -After running this task, your environment will be ready to start coding. +Some resources to get you started: + - [A quick intro to Dependency Injection](https://www.freecodecamp.org/news/a-quick-intro-to-dependency-injection-what-it-is-and-when-to-use-it-7578c84fa88f/) + - [Understanding Inversion of Control (IoC) Principle](https://medium.com/@amitkma/understanding-inversion-of-control-ioc-principle-163b1dc97454) -DO NOT remove the node_modules folder, this is an auto generated directory that has the required dependencies to be able to use typescript and more. +## **Coding Guidelines** -## **IMPORTANT:** +Focus your mod development around the `mod.ts` file. In the `package.json` file, only alter these properties: `"name"`, `"version"`, `"license"`, `"author"`, and `"akiVersion"`. -Before starting to work on your mod, we suggest you read about Dependency Injection and Inversion of Control as this is the adopted architecture SPT-AKI has adopted. +New to Typescript? Find comprehensive documentation on the [official website](https://www.typescriptlang.org/docs/). -It will be difficult to understand some of the problems you may be having if you dont understand the basics of it. +## **Distribution Guidelines** -A guide explaining all the essentials will be available on the hub on release for you to read about. +Automated tasks are set up to bundle all necessary files for your mod to function in SPT-AKI: -## **Coding:** +> Terminal -> Run Task... -> Show All Tasks... -> npm: build -All your work should be centered around the mod.ts file as an entry point. -You can ONLY change the following properties from the package.json file: `"name"`, `"version"`, `"license"`: `"MIT"`, `"author"`, `"akiVersion"`. +The ZIP output, located in the `dist` directory, contains all required files. Ensure all files are included and modify the `.buildignore` file as needed. This ZIP file is your uploadable asset for the hub. -If you have never used typescript before, you can read about it here: https://www.typescriptlang.org/docs/ +## **Conclusion** -## **Distributing your mod:** +With this setup, you're ready to begin modding with SPT-AKI. If you run into any trouble be sure to check out the [modding documentation on the hub](https://hub.sp-tarkov.com/doc/lexicon/66-modding/). If you really get stuck feel free to join us in the [#mods-development](https://discord.com/channels/875684761291599922/875803116409323562) official Discord channel. -The project has been set up with an automatic task that will copy and zip ALL required files for your mod to work on SPT-AKI. -To run this task you just need to go to: - -> Terminal->Run Task...->Show All Tasks...->npm: build:zip - -The output will be a mod.zip file that will appear on the root of the project. - -Always verify that all files were included into the zip file. \ No newline at end of file +Build something awesome! diff --git a/TypeScript/9RouterHooks/build.mjs b/TypeScript/9RouterHooks/build.mjs new file mode 100644 index 0000000..d00117c --- /dev/null +++ b/TypeScript/9RouterHooks/build.mjs @@ -0,0 +1,383 @@ +#!/usr/bin/env node + +/** + * Build Script + * + * This script automates the build process for server-side SPT mod projects, facilitating the creation of distributable + * mod packages. It performs a series of operations as outlined below: + * - Loads the .buildignore file, which is used to list files that should be ignored during the build process. + * - Loads the package.json to get project details so a descriptive name can be created for the mod package. + * - Creates a distribution directory and a temporary working directory. + * - Copies files to the temporary directory while respecting the .buildignore rules. + * - Creates a zip archive of the project files. + * - Moves the zip file to the root of the distribution directory. + * - Cleans up the temporary directory. + * + * It's typical that this script be customized to suit the needs of each project. For example, the script can be updated + * to perform additional operations, such as moving the mod package to a specific location or uploading it to a server. + * This script is intended to be a starting point for developers to build upon. + * + * Usage: + * - Run this script using npm: `npm run build` + * - Use `npm run buildinfo` for detailed logging. + * + * Note: + * - Ensure that all necessary Node.js modules are installed before running the script: `npm install` + * - The script reads configurations from the `package.json` and `.buildignore` files; ensure they are correctly set up. + * + * @author Refringe + * @version v1.0.0 + */ + +import fs from "fs-extra"; +import os from "os"; +import path from "path"; +import { fileURLToPath } from "url"; +import { dirname } from "path"; +import ignore from "ignore"; +import archiver from "archiver"; +import winston from "winston"; + +// Get the command line arguments to determine whether to use verbose logging. +const args = process.argv.slice(2); +const verbose = args.includes("--verbose") || args.includes("-v"); + +// Configure the Winston logger to use colours. +const logColors = { + error: "red", + warn: "yellow", + info: "grey", + success: "green", +}; +winston.addColors(logColors); + +// Create a logger instance to log build progress. Configure the logger levels to allow for different levels of logging +// based on the verbosity flag, and set the console transport to log messages of the appropriate level. +const logger = winston.createLogger({ + levels: { + error: 0, + warn: 1, + success: 2, + info: 3, + }, + format: winston.format.combine( + winston.format.colorize(), + winston.format.printf(info => { + return `${info.level}: ${info.message}`; + }) + ), + transports: [ + new winston.transports.Console({ + level: verbose ? "info" : "success", + }), + ], +}); + +/** + * The main function orchestrates the build process for creating a distributable mod package. It leverages a series of + * helper functions to perform various tasks such as loading configuration files, setting up directories, copying files + * according to `.buildignore` rules, and creating a ZIP archive of the project files. + * + * Utilizes the Winston logger to provide information on the build status at different stages of the process. + * + * @returns {void} + */ +async function main() { + // Get the current directory where the script is being executed + const currentDir = getCurrentDirectory(); + + // Defining at this scope because we need to use it in the finally block. + let projectDir; + + try { + // Load the .buildignore file to set up an ignore handler for the build process. + const buildIgnorePatterns = await loadBuildIgnoreFile(currentDir); + + // Load the package.json file to get project details. + const packageJson = await loadPackageJson(currentDir); + + // Create a descriptive name for the mod package. + const projectName = createProjectName(packageJson); + logger.log("success", `Project name created: ${projectName}`); + + // Remove the old distribution directory and create a fresh one. + const distDir = await removeOldDistDirectory(currentDir); + logger.log("info", "Distribution directory successfully cleaned."); + + // Create a temporary working directory to perform the build operations. + projectDir = await createTemporaryDirectoryWithProjectName(projectName); + logger.log("success", "Temporary working directory successfully created."); + logger.log("info", projectDir); + + // Copy files to the temporary directory while respecting the .buildignore rules. + logger.log("info", "Beginning copy operation using .buildignore file..."); + await copyFiles(currentDir, projectDir, buildIgnorePatterns); + logger.log("success", "Files successfully copied to temporary directory."); + + // Create a zip archive of the project files. + logger.log("info", "Beginning folder compression..."); + const zipFilePath = path.join(path.dirname(projectDir), `${projectName}.zip`); + await createZipFile(projectDir, zipFilePath, "user/mods/" + projectName); + logger.log("success", "Archive successfully created."); + logger.log("info", zipFilePath); + + // Move the zip file inside of the project directory, within the temporary working directory. + const zipFileInProjectDir = path.join(projectDir, `${projectName}.zip`); + await fs.move(zipFilePath, zipFileInProjectDir); + logger.log("success", "Archive successfully moved."); + logger.log("info", zipFileInProjectDir); + + // Move the temporary directory into the distribution directory. + await fs.move(projectDir, distDir); + logger.log("success", "Temporary directory successfully moved into project distribution directory."); + + // Log the success message. Write out the path to the mod package. + logger.log("success", "------------------------------------"); + logger.log("success", "Build script completed successfully!"); + logger.log("success", "Your mod package has been created in the 'dist' directory:"); + logger.log("success", `/${path.relative(process.cwd(), path.join(distDir, `${projectName}.zip`))}`); + logger.log("success", "------------------------------------"); + if (!verbose) { + logger.log("success", "To see a detailed build log, use `npm run buildinfo`."); + logger.log("success", "------------------------------------"); + } + } catch (err) { + // If any of the file operations fail, log the error. + logger.log("error", "An error occurred: " + err); + } finally { + // Clean up the temporary directory, even if the build fails. + if (projectDir) { + try { + await fs.promises.rm(projectDir, { force: true, recursive: true }); + logger.log("info", "Cleaned temporary directory."); + } catch (err) { + logger.log("error", "Failed to clean temporary directory: " + err); + } + } + } +} + +/** + * Retrieves the current working directory where the script is being executed. This directory is used as a reference + * point for various file operations throughout the build process, ensuring that paths are resolved correctly regardless + * of the location from which the script is invoked. + * + * @returns {string} The absolute path of the current working directory. + */ +function getCurrentDirectory() { + return dirname(fileURLToPath(import.meta.url)); +} + +/** + * Loads the `.buildignore` file and sets up an ignore handler using the `ignore` module. The `.buildignore` file + * contains a list of patterns describing files and directories that should be ignored during the build process. The + * ignore handler created by this method is used to filter files and directories when copying them to the temporary + * directory, ensuring that only necessary files are included in the final mod package. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to an ignore handler. + */ +async function loadBuildIgnoreFile(currentDir) { + const buildIgnorePath = path.join(currentDir, ".buildignore"); + + try { + // Attempt to read the contents of the .buildignore file asynchronously. + const fileContent = await fs.promises.readFile(buildIgnorePath, "utf-8"); + + // Return a new ignore instance and add the rules from the .buildignore file (split by newlines). + return ignore().add(fileContent.split("\n")); + } catch (err) { + logger.log("warn", "Failed to read .buildignore file. No files or directories will be ignored."); + + // Return an empty ignore instance, ensuring the build process can continue. + return ignore(); + } +} + +/** + * Loads the `package.json` file and returns its content as a JSON object. The `package.json` file contains important + * project details such as the name and version, which are used in later stages of the build process to create a + * descriptive name for the mod package. The method reads the file from the current working directory, ensuring that it + * accurately reflects the current state of the project. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to a JSON object containing the contents of the `package.json`. + */ +async function loadPackageJson(currentDir) { + const packageJsonPath = path.join(currentDir, "package.json"); + + // Read the contents of the package.json file asynchronously as a UTF-8 string. + const packageJsonContent = await fs.promises.readFile(packageJsonPath, "utf-8"); + + return JSON.parse(packageJsonContent); +} + +/** + * Constructs a descriptive name for the mod package using details from the `package.json` file. The name is created by + * concatenating the project name, version, and a timestamp, resulting in a unique and descriptive file name for each + * build. This name is used as the base name for the temporary working directory and the final ZIP archive, helping to + * identify different versions of the mod package easily. + * + * @param {Object} packageJson - A JSON object containing the contents of the `package.json` file. + * @returns {string} A string representing the constructed project name. + */ +function createProjectName(packageJson) { + // Remove any non-alphanumeric characters from the author and name. + const author = packageJson.author.replace(/\W/g, ""); + const name = packageJson.name.replace(/\W/g, ""); + const version = packageJson.version; + + // Ensure the name is lowercase, as per the package.json specification. + return `${author}-${name}-${version}`.toLowerCase(); +} + +/** + * Defines the location of the distribution directory where the final mod package will be stored and deletes any + * existing distribution directory to ensure a clean slate for the build process. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @returns {Promise} A promise that resolves to the absolute path to the distribution directory. + */ +async function removeOldDistDirectory(projectDir) { + const distPath = path.join(projectDir, "dist"); + await fs.remove(distPath); + return distPath; +} + +/** + * Creates a temporary working directory using the project name. This directory serves as a staging area where project + * files are gathered before being archived into the final mod package. The method constructs a unique directory path + * by appending the project name to a base temporary directory path, ensuring that each build has its own isolated + * working space. This approach facilitates clean and organized build processes, avoiding potential conflicts with other + * builds. + * + * @param {string} currentDirectory - The absolute path of the current working directory. + * @param {string} projectName - The constructed project name, used to create a unique path for the temporary directory. + * @returns {Promise} A promise that resolves to the absolute path of the newly created temporary directory. + */ +async function createTemporaryDirectoryWithProjectName(projectName) { + // Create a new directory in the system's temporary folder to hold the project files. + const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "spt-mod-build-")); + + // Create a subdirectory within the temporary directory using the project name for this specific build. + const projectDir = path.join(tempDir, projectName); + await fs.ensureDir(projectDir); + + return projectDir; +} + +/** + * Copies the project files to the temporary directory while respecting the rules defined in the `.buildignore` file. + * The method is recursive, iterating over all files and directories in the source directory and using the ignore + * handler to filter out files and directories that match the patterns defined in the `.buildignore` file. This ensures + * that only the necessary files are included in the final mod package, adhering to the specifications defined by the + * developer in the `.buildignore` file. + * + * The copy operations are delayed and executed in parallel to improve efficiency and reduce the build time. This is + * achieved by creating an array of copy promises and awaiting them all at the end of the function. + * + * @param {string} sourceDirectory - The absolute path of the current working directory. + * @param {string} destinationDirectory - The absolute path of the temporary directory where the files will be copied. + * @param {Ignore} ignoreHandler - The ignore handler created from the `.buildignore` file. + * @returns {Promise} A promise that resolves when all copy operations are completed successfully. + */ +async function copyFiles(srcDir, destDir, ignoreHandler) { + try { + // Read the contents of the source directory to get a list of entries (files and directories). + const entries = await fs.promises.readdir(srcDir, { withFileTypes: true }); + + // Initialize an array to hold the promises returned by recursive calls to copyFiles and copyFile operations. + const copyOperations = []; + + for (const entry of entries) { + // Define the source and destination paths for each entry. + const srcPath = path.join(srcDir, entry.name); + const destPath = path.join(destDir, entry.name); + + // Get the relative path of the source file to check against the ignore handler. + const relativePath = path.relative(process.cwd(), srcPath); + + // If the ignore handler dictates that this file should be ignored, skip to the next iteration. + if (ignoreHandler.ignores(relativePath)) { + logger.log("info", `Ignored: /${path.relative(process.cwd(), srcPath)}`); + continue; + } + + if (entry.isDirectory()) { + // If the entry is a directory, create the corresponding temporary directory and make a recursive call + // to copyFiles to handle copying the contents of the directory. + await fs.ensureDir(destPath); + copyOperations.push(copyFiles(srcPath, destPath, ignoreHandler)); + } else { + // If the entry is a file, add a copyFile operation to the copyOperations array and log the event when + // the operation is successful. + copyOperations.push( + fs.copy(srcPath, destPath).then(() => { + logger.log("info", `Copied: /${path.relative(process.cwd(), srcPath)}`); + }) + ); + } + } + + // Await all copy operations to ensure all files and directories are copied before exiting the function. + await Promise.all(copyOperations); + } catch (err) { + // Log an error message if any error occurs during the copy process. + logger.log("error", "Error copying files: " + err); + } +} + +/** + * Creates a ZIP archive of the project files located in the temporary directory. The method uses the `archiver` module + * to create a ZIP file, which includes all the files that have been copied to the temporary directory during the build + * process. The ZIP file is named using the project name, helping to identify the contents of the archive easily. + * + * @param {string} directoryPath - The absolute path of the temporary directory containing the project files. + * @param {string} projectName - The constructed project name, used to name the ZIP file. + * @returns {Promise} A promise that resolves to the absolute path of the created ZIP file. + */ +async function createZipFile(directoryToZip, zipFilePath, containerDirName) { + return new Promise((resolve, reject) => { + // Create a write stream to the specified ZIP file path. + const output = fs.createWriteStream(zipFilePath); + + // Create a new archiver instance with ZIP format and maximum compression level. + const archive = archiver("zip", { + zlib: { level: 9 }, + }); + + // Set up an event listener for the 'close' event to resolve the promise when the archiver has finalized. + output.on("close", function () { + logger.log("info", "Archiver has finalized. The output and the file descriptor have closed."); + resolve(); + }); + + // Set up an event listener for the 'warning' event to handle warnings appropriately, logging them or rejecting + // the promise based on the error code. + archive.on("warning", function (err) { + if (err.code === "ENOENT") { + logger.log("warn", `Archiver issued a warning: ${err.code} - ${err.message}`); + } else { + reject(err); + } + }); + + // Set up an event listener for the 'error' event to reject the promise if any error occurs during archiving. + archive.on("error", function (err) { + reject(err); + }); + + // Pipe archive data to the file. + archive.pipe(output); + + // Add the directory to the archive, under the provided directory name. + archive.directory(directoryToZip, containerDirName); + + // Finalize the archive, indicating that no more files will be added and triggering the 'close' event once all + // data has been written. + archive.finalize(); + }); +} + +// Engage! +main(); diff --git a/TypeScript/9RouterHooks/package.json b/TypeScript/9RouterHooks/package.json index 8514193..21edea5 100644 --- a/TypeScript/9RouterHooks/package.json +++ b/TypeScript/9RouterHooks/package.json @@ -7,17 +7,20 @@ "akiVersion": "~3.7", "scripts": { "setup": "npm i", - "build": "node ./packageBuild.ts" + "build": "node ./build.mjs", + "buildinfo": "node ./build.mjs --verbose" }, "devDependencies": { "@types/node": "20.4.5", "@typescript-eslint/eslint-plugin": "6.2.0", "@typescript-eslint/parser": "6.2.0", - "bestzip": "2.2.1", + "archiver": "^6.0", "eslint": "8.46.0", - "fs-extra": "11.1.1", - "glob": "10.3.3", + "fs-extra": "^11.1", + "ignore": "^5.2", + "os": "^0.1", "tsyringe": "4.8.0", - "typescript": "5.1.6" + "typescript": "5.1.6", + "winston": "^3.9" } -} \ No newline at end of file +} diff --git a/TypeScript/9RouterHooks/packageBuild.ts b/TypeScript/9RouterHooks/packageBuild.ts deleted file mode 100644 index d76ddd8..0000000 --- a/TypeScript/9RouterHooks/packageBuild.ts +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env node - -// This is a simple script used to build a mod package. The script will copy necessary files to the build directory -// and compress the build directory into a zip file that can be easily shared. - -const fs = require("fs-extra"); -const glob = require("glob"); -const zip = require('bestzip'); -const path = require("path"); -const minimatch = require('minimatch'); - -// Load the package.json file to get some information about the package so we can name things appropriately. This is -// atypical, and you would never do this in a production environment, but this script is only used for development so -// it's fine in this case. Some of these values are stored in environment variables, but those differ between node -// versions; the 'author' value is not available after node v14. -const { author, name:packageName, version } = require("./package.json"); - -// Generate the name of the package, stripping out all non-alphanumeric characters in the 'author' and 'name'. -const modName = `${author.replace(/[^a-z0-9]/gi, "")}-${packageName.replace(/[^a-z0-9]/gi, "")}-${version}`; -console.log(`Generated package name: ${modName}`); - -// Delete the old build directory and compressed package file. -fs.rmSync(`${__dirname}/dist`, { force: true, recursive: true }); -console.log("Previous build files deleted."); - -// Generate a list of files that should not be copied over into the distribution directory. This is a blacklist to ensure -// we always copy over additional files and directories that authors may have added to their project. This may need to be -// expanded upon by the mod author to allow for node modules that are used within the mod; example commented out below. -const ignoreList = [ - "node_modules/", - // "node_modules/!(weighted|glob)", // Instead of excluding the entire node_modules directory, allow two node modules. - "src/**/*.js", - "types/", - ".git/", - ".gitea/", - ".eslintignore", - ".eslintrc.json", - ".gitignore", - ".DS_Store", - "packageBuild.ts", - "mod.code-workspace", - "package-lock.json", - "tsconfig.json" -]; -const exclude = glob.sync(`{${ignoreList.join(",")}}`, { realpath: true, dot: true }); - -fs.copySync(__dirname, path.normalize(`${__dirname}/../~${modName}`), {filter: (src) => -{ - const relativePath = path.relative(__dirname, src); - const shouldExclude = exclude.some((pattern) => minimatch(relativePath, pattern)); - console.log(`${relativePath} - Excluded: ${shouldExclude}`); - return !shouldExclude; -},}); - -fs.moveSync(path.normalize(`${__dirname}/../~${modName}`), path.normalize(`${__dirname}/${modName}`), { overwrite: true }); -fs.copySync(path.normalize(`${__dirname}/${modName}`), path.normalize(`${__dirname}/dist`)); -console.log("Build files copied."); - -// Compress the files for easy distribution. The compressed file is saved into the dist directory. When uncompressed we -// need to be sure that it includes a directory that the user can easily copy into their game mods directory. -zip({ - source: modName, - destination: `dist/${modName}.zip`, - cwd: __dirname -}).catch(function(err) -{ - console.error("A bestzip error has occurred: ", err.stack); -}).then(function() -{ - console.log(`Compressed mod package to: /dist/${modName}.zip`); - - // Now that we're done with the compression we can delete the temporary build directory. - fs.rmSync(`${__dirname}/${modName}`, { force: true, recursive: true }); - console.log("Build successful! your zip file has been created and is ready to be uploaded to hub.sp-tarkov.com/files/"); -}); \ No newline at end of file