UNPKG

nx

Version:

The core Nx plugin contains the core functionality of Nx like the project graph, nx commands and task orchestration.

310 lines (309 loc) • 11.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.FsTree = void 0; exports.flushChanges = flushChanges; exports.printChanges = printChanges; const node_fs_1 = require("node:fs"); const logger_1 = require("../utils/logger"); const output_1 = require("../utils/output"); const path_1 = require("path"); const chalk = require("chalk"); class FsTree { constructor(root, isVerbose, logOperationId) { this.root = root; this.isVerbose = isVerbose; this.logOperationId = logOperationId; this.recordedChanges = {}; /** * Signifies if operations on the tree instance * are allowed. Set to false after changes are written * to disk, to prevent someone trying to use the tree to update * files when the tree is no longer effective. */ this.locked = false; } read(filePath, encoding) { filePath = this.normalize(filePath); try { let content; if (this.recordedChanges[this.rp(filePath)]) { content = this.recordedChanges[this.rp(filePath)].content; } else { content = this.fsReadFile(filePath); } return encoding ? content.toString(encoding) : content; } catch (e) { if (this.isVerbose) { logger_1.logger.error(e); } return null; } } write(filePath, content, options) { this.assertUnlocked(); filePath = this.normalize(filePath); // Remove any recorded changes where a parent directory has been // deleted when writing a new file within the directory. let parent = (0, path_1.dirname)(this.rp(filePath)); while (parent !== '.') { if (this.recordedChanges[parent]?.isDeleted) { delete this.recordedChanges[parent]; } parent = (0, path_1.dirname)(parent); } if (this.fsExists(this.rp(filePath)) && Buffer.from(content).equals(this.fsReadFile(filePath))) { // Remove recorded change because the file has been restored to it's original contents delete this.recordedChanges[this.rp(filePath)]; return; } try { this.recordedChanges[this.rp(filePath)] = { content: Buffer.from(content), isDeleted: false, options, }; } catch (e) { if (this.isVerbose) { logger_1.logger.error(e); } } } overwrite(filePath, content, options) { filePath = this.normalize(filePath); this.write(filePath, content, options); } delete(filePath) { this.assertUnlocked(); filePath = this.normalize(filePath); if (this.filesForDir(this.rp(filePath)).length > 0) { this.filesForDir(this.rp(filePath)).forEach((f) => (this.recordedChanges[f] = { content: null, isDeleted: true })); } this.recordedChanges[this.rp(filePath)] = { content: null, isDeleted: true, }; // Delete directory when is not root and there are no children if (filePath !== '' && this.children((0, path_1.dirname)(this.rp(filePath))).length < 1) { this.delete((0, path_1.dirname)(this.rp(filePath))); } } exists(filePath) { filePath = this.normalize(filePath); try { if (this.recordedChanges[this.rp(filePath)]) { return !this.recordedChanges[this.rp(filePath)].isDeleted; } else if (this.filesForDir(this.rp(filePath)).length > 0) { return true; } else { return this.fsExists(filePath); } } catch { return false; } } rename(from, to) { this.assertUnlocked(); from = this.normalize(from); to = this.normalize(to); if (from === to) { return; } if (this.isFile(from)) { const content = this.read(this.rp(from)); this.write(this.rp(to), content); this.delete(this.rp(from)); } else { for (const child of this.children(from)) { this.rename((0, path_1.join)(from, child), (0, path_1.join)(to, child)); } } } isFile(filePath) { filePath = this.normalize(filePath); try { if (this.recordedChanges[this.rp(filePath)]) { return !this.recordedChanges[this.rp(filePath)].isDeleted; } else { return this.fsIsFile(filePath); } } catch { return false; } } children(dirPath) { dirPath = this.normalize(dirPath); let res = this.fsReadDir(dirPath); res = [...res, ...this.directChildrenOfDir(this.rp(dirPath))]; res = res.filter((q) => { const r = this.recordedChanges[this.normalize((0, path_1.join)(this.rp(dirPath), q))]; return !r?.isDeleted; }); // Dedupe return Array.from(new Set(res)); } listChanges() { const res = []; Object.keys(this.recordedChanges).forEach((f) => { if (this.recordedChanges[f].isDeleted) { if (this.fsExists(f)) { res.push({ path: f, type: 'DELETE', content: null }); } } else { if (this.fsExists(f)) { res.push({ path: f, type: 'UPDATE', content: this.recordedChanges[f].content, options: this.recordedChanges[f].options, }); } else { res.push({ path: f, type: 'CREATE', content: this.recordedChanges[f].content, options: this.recordedChanges[f].options, }); } } }); return res; } changePermissions(filePath, mode) { this.assertUnlocked(); filePath = this.normalize(filePath); const filePathChangeKey = this.rp(filePath); if (this.recordedChanges[filePathChangeKey]) { if (this.recordedChanges[filePathChangeKey].isDeleted) { throw new Error(`Cannot change permissions of deleted file ${filePath}.`); } this.recordedChanges[filePathChangeKey].options = { mode }; } else if (!this.fsExists(filePath)) { throw new Error(`Cannot change permissions of non-existing file ${filePath}.`); } else if (!this.fsIsFile(filePath)) { // To fully support directories we'd need to change how we store // changes to keep a record of directories so we can associate // permissions to them. throw new Error(`Cannot change permissions of non-file ${filePath}.`); } else { this.recordedChanges[filePathChangeKey] = { content: this.fsReadFile(filePath), isDeleted: false, options: { mode }, }; } } // Marks FsTree as final. lock() { this.locked = true; } assertUnlocked() { if (this.locked) { output_1.output.error({ title: `File changes have already been written to disk. Further changes were attempted ${this.logOperationId ? ` while running ${this.logOperationId}.` : '.'}`, bodyLines: [ 'The file system can no longer be modified. This commonly happens when a generator attempts to make further changes in its callback, or an asynchronous operation is still running after the generator completes.', ], }); throw new Error('Tree changed after commit to disk.'); } } normalize(path) { return (0, path_1.relative)(this.root, (0, path_1.join)(this.root, path)).split(path_1.sep).join('/'); } fsReadDir(dirPath) { try { return (0, node_fs_1.readdirSync)((0, path_1.join)(this.root, dirPath)); } catch { return []; } } fsIsFile(filePath) { const stat = (0, node_fs_1.statSync)((0, path_1.join)(this.root, filePath)); return stat.isFile(); } fsReadFile(filePath) { return (0, node_fs_1.readFileSync)((0, path_1.join)(this.root, filePath)); } fsExists(filePath) { try { const stat = (0, node_fs_1.statSync)((0, path_1.join)(this.root, filePath)); return stat.isFile() || stat.isDirectory(); } catch { return false; } } filesForDir(path) { return Object.keys(this.recordedChanges).filter((f) => f.startsWith(`${path}/`) && !this.recordedChanges[f].isDeleted); } directChildrenOfDir(path) { const res = {}; if (path === '') { return Object.keys(this.recordedChanges).map((file) => file.split('/')[0]); } Object.keys(this.recordedChanges).forEach((f) => { if (f.startsWith(`${path}/`)) { // Remove the current folder's path from the directory const file = f.substring(path.length + 1); // Split the path on segments, and take the first one const basePath = file.split('/')[0]; // Mark it as a child of the current directory res[basePath] = true; } }); return Object.keys(res); } rp(pp) { return pp.startsWith('/') ? pp.substring(1) : pp; } } exports.FsTree = FsTree; function flushChanges(root, fileChanges) { fileChanges.forEach((f) => { const fpath = (0, path_1.join)(root, f.path); if (f.type === 'CREATE') { (0, node_fs_1.mkdirSync)((0, path_1.dirname)(fpath), { recursive: true }); (0, node_fs_1.writeFileSync)(fpath, f.content); if (f.options?.mode) (0, node_fs_1.chmodSync)(fpath, f.options.mode); } else if (f.type === 'UPDATE') { (0, node_fs_1.writeFileSync)(fpath, f.content); if (f.options?.mode) (0, node_fs_1.chmodSync)(fpath, f.options.mode); } else if (f.type === 'DELETE') { (0, node_fs_1.rmSync)(fpath, { recursive: true, force: true }); } }); } function printChanges(fileChanges, indent = '') { fileChanges.forEach((f) => { if (f.type === 'CREATE') { console.log(`${indent}${chalk.green('CREATE')} ${f.path}`); } else if (f.type === 'UPDATE') { console.log(`${indent}${chalk.white('UPDATE')} ${f.path}`); } else if (f.type === 'DELETE') { console.log(`${indent}${chalk.yellow('DELETE')} ${f.path}`); } }); }