UNPKG

@bscotch/stitch

Version:

Stitch: The GameMaker Studio 2 Asset Pipeline Development Kit.

225 lines 8.25 kB
import { pathy } from '@bscotch/pathy'; import { oneline } from '@bscotch/utility'; import { Yy } from '@bscotch/yy'; import { ok } from 'assert'; import child_process from 'child_process'; import { difference } from 'lodash-es'; import { StitchError, assert } from '../utility/errors.js'; import fs from '../utility/files.js'; import { debug } from '../utility/log.js'; import paths from '../utility/paths.js'; /** * A class for centralizing file system i/o on GMS2 * projects, so that we can more easily run in read-only * mode, attach hooks to file i/o events, etc. */ export class StitchStorage { _yypPathAbsolute; isReadOnly; bypassGitRequirement; constructor(_yypPathAbsolute, isReadOnly = false, bypassGitRequirement = false) { this._yypPathAbsolute = _yypPathAbsolute; this.isReadOnly = isReadOnly; this.bypassGitRequirement = bypassGitRequirement; if (!bypassGitRequirement && process.env.GMS2PDK_DEV != 'true' && !this.workingDirIsClean) { throw new StitchError(oneline ` Working directory for ${paths.basename(_yypPathAbsolute)} is not clean. Commit or stash your work! `); } } get yypPathAbsolute() { return this._yypPathAbsolute; } renameYypFile(newName) { ok(!this.isReadOnly, 'Cannot rename YYP file in read-only mode.'); newName = newName.replace(/\.yyp$/, ''); ok(!newName.match(/[/\\]/), 'New name cannot contain slashes! It must be a valid filename.'); const oldPath = this.yypPathAbsolute; this._yypPathAbsolute = paths.join(this.yypDirAbsolute, newName + '.yyp'); fs.moveSync(oldPath, this.yypPathAbsolute); return this._yypPathAbsolute; } get yypDirAbsolute() { return paths.dirname(this.yypPathAbsolute); } get workingDirIsClean() { const gitProcessHandle = child_process.spawnSync(`git status`, { cwd: this.yypDirAbsolute, shell: true, }); if (gitProcessHandle.status != 0) { throw new StitchError(gitProcessHandle.stderr.toString()); } else { const isClean = gitProcessHandle.stdout .toString() .includes('working tree clean'); return isClean; } } get gitWorkingTreeRoot() { const gitProcessHandle = child_process.spawnSync(`git worktree list`, { cwd: this.yypDirAbsolute, shell: true, }); if (gitProcessHandle.status != 0) { throw new StitchError(gitProcessHandle.stderr.toString()); } return gitProcessHandle.stdout.toString().split(/\s+/g)[0]; } toAbsolutePath(pathRelativeToYypDir) { return paths.join(this.yypDirAbsolute, pathRelativeToYypDir); } ensureDirSync(dir) { if (!this.isReadOnly) { fs.ensureDirSync(dir); } } /** Delete all files and folders (recursively) inside this directory. */ emptyDirSync(dir, includeStartingDir = false) { if (!this.isReadOnly) { if (!fs.existsSync(dir)) { return; } fs.emptyDirSync(dir); if (includeStartingDir) { fs.removeSync(dir); } } } deleteFileSync(path) { if (!this.isReadOnly) { fs.removeSync(path); } } listFilesSync(dir, recursive, allowedExtension) { if (allowedExtension && allowedExtension.length > 0) { return fs.listFilesByExtensionSync(dir, allowedExtension, recursive); } else { return fs.listFilesSync(dir, recursive); } } listPathsSync(dir, recursive) { return fs.listPathsSync(dir, recursive); } /** * Copy a file or recursively copy a directory. * Files are only overwritten if there is a change. */ copySync(from, to, options) { assert(fs.existsSync(from), `Cannot copy from ${from}, path does not exist.`); if (fs.isFileSync(from)) { if (!fs.existsSync(to) || fs.checksum(from) != fs.checksum(to)) { fs.copySync(from, to, { overwrite: true }); } return; } // If destination doesn't exist we can just copy over. if (!fs.existsSync(to) || !fs.listPathsSync(to, true).length) { fs.mkdirSync(paths.dirname(to), { recursive: true }); return fs.copySync(from, to, { overwrite: true }); } // Else we need to diff the source and destination, // remove any extra files from destination, and write // new/updated files. const fromFiles = fs .listFilesSync(from, true) .map((p) => paths.relative(from, p)); const toFiles = fs .listFilesSync(to, true) .map((p) => paths.relative(to, p)); if (!options?.sparse) { // Delete extra files in destination difference(toFiles, fromFiles).forEach((toDelete) => fs.removeSync(paths.join(to, toDelete))); } // Copy files that have changed or are new fromFiles.forEach((fromFileRelative) => { const fromAbs = paths.join(from, fromFileRelative); const toAbs = paths.join(to, fromFileRelative); fs.ensureDirSync(paths.dirname(toAbs)); if (!fs.existsSync(toAbs) || fs.checksum(toAbs) != fs.checksum(fromAbs)) { fs.copySync(fromAbs, toAbs, { overwrite: true }); } }); } existsSync(path) { return fs.existsSync(path); } isFileSync(path) { return fs.statSync(path).isFile(); } isDirectorySync(path) { return fs.statSync(path).isDirectory(); } copyFileSync(sourceOrPaths, destinationPath) { if (typeof sourceOrPaths != 'string') { [sourceOrPaths, destinationPath] = sourceOrPaths; } assert(fs.existsSync(sourceOrPaths), `copyFile: source ${sourceOrPaths} does not exist`); if (!this.isReadOnly) { fs.ensureDirSync(paths.dirname(destinationPath)); // // Not sure why the file was being deleted first, since // // it gets clobbered by the copy anyway. The extra file operation // // with the delete could trigger unwanted side effects via the // // GMS2 file watcher. // fs.removeSync(destinationPath as string); fs.copyFileSync(sourceOrPaths, destinationPath); } } asPosixPath(path) { return paths.asPosixPath(path); } writeBlobSync(filePath, data, eol) { if (!this.isReadOnly) { fs.writeFileSync(filePath, eol && typeof data == 'string' ? data.replace(/\r?\n/gm, eol) : data); } } /** * Write data as plain JSON */ writeJsonSync(filePath, data, schema) { if (!this.isReadOnly) { fs.writeFileSync(filePath, JSON.stringify(schema ? schema.parse(data) : data, null, 2)); } } /** * @returns true if the file was written, false if not (e.g. read-only mode or no change) */ writeYySync(filePath, data, type, yyp) { if (!this.isReadOnly) { if (Yy.writeSync(filePath, data, type, yyp)) { debug(`Wrote file ${paths.basename(filePath)}`); return true; } } return false; } async writeYy(filePath, data, type, yyp) { if (!this.isReadOnly) { if (await Yy.write(filePath, data, type, yyp)) { debug(`Wrote file ${paths.basename(filePath)}`); } } } readBlobSync(filePath) { return fs.readFileSync(filePath); } readTextSync(filePath) { return fs.readFileSync(filePath, 'utf8'); } readJsonSync(filePath, schema) { return Yy.readSync(filePath, schema); } async readJson(filePath, options) { return await pathy(filePath).read(options); } async writeJson(filePath, data, options) { if (!this.isReadOnly) { await pathy(filePath).write(data, options); } } } //# sourceMappingURL=StitchStorage.js.map