@bscotch/stitch
Version:
Stitch: The GameMaker Studio 2 Asset Pipeline Development Kit.
225 lines • 8.25 kB
JavaScript
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