0
0
mirror of https://github.com/sp-tarkov/server.git synced 2025-02-13 09:50:43 -05:00
server/project/src/services/ModCompilerService.ts
Refringe ed8dbbd195 Adds Biome - Removes ESLint & Prettier (!383)
Boogidy, boogidy, boogidy. Let's go racing! 🏎️

Removes the over-complicated and super-slow setup we had with ESLint & Prettier in favour of Biome. The largest change with the formatting is moving from Allman braces to 1TBS braces. Other than that, it's *pretty much* the same. Ah, and that Biome runs formatting and linting on the entire project about x10 faster than the old system ran formatting on one file. Seriously, the guy who came up with that last solution should be fired. :runs:

I've kept all of the formatting and linting commands the same as before, with the main mamma-jamma being: `npm run format`, which applies formatting and linting changes to the entire project.

Formatting-on-save works (quickly!) by (1) ensuring that you're working within the VSC workspace (as you should be), and (2) have the recommended Biome VSC extension installed. The link to the Biome extension is in the README.

This limits our options on code formatting going forward; Biome, like prettier, is very opinionated with very few formatting options available. But I see this as a good thing. I'd rather spend my time arguing about which gun in Tarkov is the best, rather than coding brace styles...

...It's the TOZ, and it always will be. Don't DM me.

Co-authored-by: chomp <chomp@noreply.dev.sp-tarkov.com>
Reviewed-on: SPT/Server#383
Co-authored-by: Refringe <me@refringe.com>
Co-committed-by: Refringe <me@refringe.com>
2024-07-22 21:15:57 +00:00

154 lines
5.1 KiB
TypeScript

import fs from "node:fs";
import path from "node:path";
import { inject, injectable } from "tsyringe";
import { ScriptTarget, ModuleKind, ModuleResolutionKind, transpileModule, CompilerOptions } from "typescript";
import type { ILogger } from "@spt/models/spt/utils/ILogger";
import { ModHashCacheService } from "@spt/services/cache/ModHashCacheService";
import { VFS } from "@spt/utils/VFS";
@injectable()
export class ModCompilerService
{
protected serverDependencies: string[];
constructor(
@inject("PrimaryLogger") protected logger: ILogger,
@inject("ModHashCacheService") protected modHashCacheService: ModHashCacheService,
@inject("VFS") protected vfs: VFS,
)
{
const packageJsonPath: string = path.join(__dirname, "../../package.json");
this.serverDependencies = Object.keys(JSON.parse(this.vfs.readFile(packageJsonPath)).dependencies);
}
/**
* Convert a mods TS into JS
* @param modName Name of mod
* @param modPath Dir path to mod
* @param modTypeScriptFiles
* @returns
*/
public async compileMod(modName: string, modPath: string, modTypeScriptFiles: string[]): Promise<void>
{
// Concatenate TS files into one string
let tsFileContents = "";
let fileExists = true; // does every js file exist (been compiled before)
for (const file of modTypeScriptFiles)
{
const fileContent = this.vfs.readFile(file);
tsFileContents += fileContent;
// Does equivalent .js file exist
if (!this.vfs.exists(file.replace(".ts", ".js")))
{
fileExists = false;
}
}
const hashMatches = this.modHashCacheService.calculateAndCompareHash(modName, tsFileContents);
if (fileExists && hashMatches)
{
// Everything exists and matches, escape early
return;
}
if (!hashMatches)
{
// Store / update hash in json file
this.modHashCacheService.calculateAndStoreHash(modName, tsFileContents);
}
return this.compile(modTypeScriptFiles, {
noEmitOnError: true,
noImplicitAny: false,
target: ScriptTarget.ES2022,
module: ModuleKind.CommonJS,
moduleResolution: ModuleResolutionKind.Node10,
sourceMap: true,
resolveJsonModule: true,
allowJs: true,
esModuleInterop: true,
downlevelIteration: true,
experimentalDecorators: true,
emitDecoratorMetadata: true,
rootDir: modPath,
});
}
/**
* Convert a TS file into JS
* @param fileNames Paths to TS files
* @param options Compiler options
*/
protected async compile(fileNames: string[], options: CompilerOptions): Promise<void>
{
// C:/snapshot/project || /snapshot/project
const baseDir: string = __dirname.replace(/\\/g, "/").split("/").slice(0, 3).join("/");
for (const filePath of fileNames)
{
const destPath = filePath.replace(".ts", ".js");
const parsedPath = path.parse(filePath);
const parsedDestPath = path.parse(destPath);
const text = fs.readFileSync(filePath).toString();
let replacedText: string;
if (globalThis.G_RELEASE_CONFIGURATION)
{
replacedText = text.replace(/(@spt)/g, `${baseDir}/obj`);
for (const dependency of this.serverDependencies)
{
replacedText = replacedText.replace(`"${dependency}"`, `"${baseDir}/node_modules/${dependency}"`);
}
}
else
{
replacedText = text.replace(/(@spt)/g, path.join(__dirname, "..").replace(/\\/g, "/"));
}
const output = transpileModule(replacedText, { compilerOptions: options });
if (output.sourceMapText)
{
output.outputText = output.outputText.replace(
"//# sourceMappingURL\=module.js.map",
`//# sourceMappingURL\=${parsedDestPath.base}.map`,
);
const sourceMap = JSON.parse(output.sourceMapText);
sourceMap.file = parsedDestPath.base;
sourceMap.sources = [parsedPath.base];
fs.writeFileSync(`${destPath}.map`, JSON.stringify(sourceMap));
}
fs.writeFileSync(destPath, output.outputText);
}
while (!this.areFilesReady(fileNames))
{
await this.delay(200);
}
}
/**
* Do the files at the provided paths exist
* @param fileNames
* @returns
*/
protected areFilesReady(fileNames: string[]): boolean
{
return fileNames.filter((x) => !this.vfs.exists(x.replace(".ts", ".js"))).length === 0;
}
/**
* Wait the provided number of milliseconds
* @param ms Milliseconds
* @returns
*/
protected delay(ms: number): Promise<unknown>
{
return new Promise((resolve) => setTimeout(resolve, ms));
}
}