The current repo belongs to Paused status, and some functions are restricted. For details, please refer to the description of repo status
Create your Gitee Account
Explore and code with more than 12 million developers,Free private repositories !:)
Sign up
文件
This repository doesn't specify license. Please pay attention to the specific project description and its upstream code dependency when using it.
Clone or Download
build.mjs 18.42 KB
Copy Edit Raw Blame History
zg authored 2024-02-03 11:29 . 输出字符串格式化
#!/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<ignore>} 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<Object>} 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<string>} 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<string>} 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<void>} 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 (entry.name === 'package.json') {
let packageJson = await loadPackageJson(srcDir);
if (packageJson != null) {
// 移除依赖项
packageJson['dependencies'] = {};
await fs.outputFile(destPath, JSON.stringify(packageJson, null, 2))
continue;
}
}
// 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<string>} 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();
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化