UNPKG

@quenty/nevermore-template-helpers

Version:

Helpers to generate Nevermore package and game templates

158 lines (135 loc) 4.56 kB
import { execa } from 'execa'; import * as fs from 'fs/promises'; import * as os from 'os'; import * as path from 'path'; import { OutputHelper } from '@quenty/cli-output-helpers'; export interface RojoBuildOptions { /** Absolute path to the rojo project JSON file */ projectPath: string; /** -o flag: absolute path to output file (.rbxl / .rbxm) */ output?: string; /** --plugin flag: filename placed in Studio's plugins folder */ plugin?: string; /** Absolute path to Studio's plugins folder (required when plugin is set) */ pluginsFolder?: string; } export interface BuildContextOptions { /** mkdtemp prefix (e.g. 'rojo-build-') */ prefix?: string; } /** * Manages a build directory lifecycle for rojo builds. * Handles temp directory creation/cleanup and persistent build directories. */ export class BuildContext { private readonly _targetdir: string; private _cleaned = false; private readonly _trackedFiles: string[] = []; private constructor(dir: string) { this._targetdir = dir; } /** * Create and initialize a BuildContext. The directory is ready to use * when this resolves. */ static async createAsync( options: BuildContextOptions = {} ): Promise<BuildContext> { const prefix = options.prefix ?? 'build-'; const dir = await fs.mkdtemp(path.join(os.tmpdir(), prefix)); return new BuildContext(dir); } /** Absolute path to the managed build directory. */ get buildDir(): string { return this._targetdir; } /** Resolve a relative path within the build directory. */ resolvePath(relativePath: string): string { return path.join(this._targetdir, relativePath); } /** * Run rojo build using this context's directory. * Returns the full plugin output path when in plugin mode, undefined otherwise. */ async rojoBuildAsync(options: RojoBuildOptions): Promise<string | undefined> { const { projectPath, output, plugin, pluginsFolder } = options; if (output && plugin) { throw new Error( 'rojoBuildAsync: specify either output or plugin, not both' ); } if (!output && !plugin) { throw new Error('rojoBuildAsync: must specify either output or plugin'); } if (plugin && !pluginsFolder) { throw new Error( 'rojoBuildAsync: plugin requires pluginsFolder for cleanup tracking' ); } const args = ['build', projectPath]; // On Linux, rojo's --plugin flag is not supported. Build to a temp // file with -o and copy to the plugins folder ourselves. const usePluginFallback = plugin && process.platform === 'linux'; if (output) { args.push('-o', output); } else if (usePluginFallback) { const tempOutput = path.join(this._targetdir, plugin); args.push('-o', tempOutput); } else if (plugin) { args.push('--plugin', plugin); } await execa('rojo', args); if (plugin && pluginsFolder) { const pluginPath = path.join(pluginsFolder, plugin); if (usePluginFallback) { const tempOutput = path.join(this._targetdir, plugin); await fs.mkdir(pluginsFolder, { recursive: true }); await fs.copyFile(tempOutput, pluginPath); } this._trackedFiles.push(pluginPath); return pluginPath; } return undefined; } /** * Execute a Lune transform script with the given arguments. */ async executeLuneTransformScriptAsync( scriptPath: string, ...args: string[] ): Promise<void> { await execa('lune', ['run', scriptPath, ...args]); } /** * Write a file into the build directory. * @returns Absolute path to the written file. */ async writeFileAsync(relativePath: string, content: string): Promise<string> { const fullPath = path.join(this._targetdir, relativePath); await fs.mkdir(path.dirname(fullPath), { recursive: true }); await fs.writeFile(fullPath, content, 'utf-8'); return fullPath; } /** * Clean up the build directory and tracked files. Idempotent — safe to call multiple times. */ async cleanupAsync(): Promise<void> { if (this._cleaned) return; this._cleaned = true; for (const filePath of this._trackedFiles) { try { await fs.unlink(filePath); } catch { // best effort — file may already be gone } } try { OutputHelper.verbose( `[Build] Cleaning up build directory: ${this._targetdir}` ); await fs.rm(this._targetdir, { recursive: true, force: true }); } catch { // best effort } } }