UNPKG

@angular-devkit/core

Version:

Angular DevKit - Core Utility Library

315 lines (314 loc) 12.4 kB
"use strict"; /** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ Object.defineProperty(exports, "__esModule", { value: true }); exports.CordHost = void 0; const rxjs_1 = require("rxjs"); const exception_1 = require("../../exception"); const memory_1 = require("./memory"); /** * A Host that records changes to the underlying Host, while keeping a record of Create, Overwrite, * Rename and Delete of files. * * This is fully compatible with Host, but will keep a staging of every changes asked. That staging * follows the principle of the Tree (e.g. can create a file that already exists). * * Using `create()` and `overwrite()` will force those operations, but using `write` will add * the create/overwrite records IIF the files does/doesn't already exist. */ class CordHost extends memory_1.SimpleMemoryHost { _back; _filesToCreate = new Set(); _filesToRename = new Map(); _filesToRenameRevert = new Map(); _filesToDelete = new Set(); _filesToOverwrite = new Set(); constructor(_back) { super(); this._back = _back; } get backend() { return this._back; } get capabilities() { // Our own host is always Synchronous, but the backend might not be. return { synchronous: this._back.capabilities.synchronous, }; } /** * Create a copy of this host, including all actions made. * @returns {CordHost} The carbon copy. */ clone() { const dolly = new CordHost(this._back); dolly._cache = new Map(this._cache); dolly._filesToCreate = new Set(this._filesToCreate); dolly._filesToRename = new Map(this._filesToRename); dolly._filesToRenameRevert = new Map(this._filesToRenameRevert); dolly._filesToDelete = new Set(this._filesToDelete); dolly._filesToOverwrite = new Set(this._filesToOverwrite); return dolly; } /** * Commit the changes recorded to a Host. It is assumed that the host does have the same structure * as the host that was used for backend (could be the same host). * @param host The host to create/delete/rename/overwrite files to. * @param force Whether to skip existence checks when creating/overwriting. This is * faster but might lead to incorrect states. Because Hosts natively don't support creation * versus overwriting (it's only writing), we check for existence before completing a request. * @returns An observable that completes when done, or error if an error occured. */ commit(host, force = false) { // Really commit everything to the actual host. return (0, rxjs_1.from)(this.records()).pipe((0, rxjs_1.concatMap)((record) => { switch (record.kind) { case 'delete': return host.delete(record.path); case 'rename': return host.rename(record.from, record.to); case 'create': return host.exists(record.path).pipe((0, rxjs_1.switchMap)((exists) => { if (exists && !force) { return (0, rxjs_1.throwError)(new exception_1.FileAlreadyExistException(record.path)); } else { return host.write(record.path, record.content); } })); case 'overwrite': return host.exists(record.path).pipe((0, rxjs_1.switchMap)((exists) => { if (!exists && !force) { return (0, rxjs_1.throwError)(new exception_1.FileDoesNotExistException(record.path)); } else { return host.write(record.path, record.content); } })); } }), (0, rxjs_1.reduce)(() => { })); } records() { return [ ...[...this._filesToDelete.values()].map((path) => ({ kind: 'delete', path, })), ...[...this._filesToRename.entries()].map(([from, to]) => ({ kind: 'rename', from, to, })), ...[...this._filesToCreate.values()].map((path) => ({ kind: 'create', path, content: this._read(path), })), ...[...this._filesToOverwrite.values()].map((path) => ({ kind: 'overwrite', path, content: this._read(path), })), ]; } /** * Specialized version of {@link CordHost#write} which forces the creation of a file whether it * exists or not. * @param {} path * @param {FileBuffer} content * @returns {Observable<void>} */ create(path, content) { if (super._exists(path)) { throw new exception_1.FileAlreadyExistException(path); } if (this._filesToDelete.has(path)) { this._filesToDelete.delete(path); this._filesToOverwrite.add(path); } else { this._filesToCreate.add(path); } return super.write(path, content); } overwrite(path, content) { return this.isDirectory(path).pipe((0, rxjs_1.switchMap)((isDir) => { if (isDir) { return (0, rxjs_1.throwError)(new exception_1.PathIsDirectoryException(path)); } return this.exists(path); }), (0, rxjs_1.switchMap)((exists) => { if (!exists) { return (0, rxjs_1.throwError)(new exception_1.FileDoesNotExistException(path)); } if (!this._filesToCreate.has(path)) { this._filesToOverwrite.add(path); } return super.write(path, content); })); } write(path, content) { return this.exists(path).pipe((0, rxjs_1.switchMap)((exists) => { if (exists) { // It exists, but might be being renamed or deleted. In that case we want to create it. if (this.willRename(path) || this.willDelete(path)) { return this.create(path, content); } else { return this.overwrite(path, content); } } else { return this.create(path, content); } })); } read(path) { if (this._exists(path)) { return super.read(path); } return this._back.read(path); } delete(path) { if (this._exists(path)) { if (this._filesToCreate.has(path)) { this._filesToCreate.delete(path); } else if (this._filesToOverwrite.has(path)) { this._filesToOverwrite.delete(path); this._filesToDelete.add(path); } else { const maybeOrigin = this._filesToRenameRevert.get(path); if (maybeOrigin) { this._filesToRenameRevert.delete(path); this._filesToRename.delete(maybeOrigin); this._filesToDelete.add(maybeOrigin); } else { return (0, rxjs_1.throwError)(new exception_1.UnknownException(`This should never happen. Path: ${JSON.stringify(path)}.`)); } } return super.delete(path); } else { return this._back.exists(path).pipe((0, rxjs_1.switchMap)((exists) => { if (exists) { this._filesToDelete.add(path); return (0, rxjs_1.of)(); } else { return (0, rxjs_1.throwError)(new exception_1.FileDoesNotExistException(path)); } })); } } rename(from, to) { return (0, rxjs_1.concat)(this.exists(to), this.exists(from)).pipe((0, rxjs_1.toArray)(), (0, rxjs_1.switchMap)(([existTo, existFrom]) => { if (!existFrom) { return (0, rxjs_1.throwError)(new exception_1.FileDoesNotExistException(from)); } if (from === to) { return rxjs_1.EMPTY; } if (existTo) { return (0, rxjs_1.throwError)(new exception_1.FileAlreadyExistException(to)); } // If we're renaming a file that's been created, shortcircuit to creating the `to` path. if (this._filesToCreate.has(from)) { this._filesToCreate.delete(from); this._filesToCreate.add(to); return super.rename(from, to); } if (this._filesToOverwrite.has(from)) { this._filesToOverwrite.delete(from); // Recursively call this function. This is so we don't repeat the bottom logic. This // if will be by-passed because we just deleted the `from` path from files to overwrite. return (0, rxjs_1.concat)(this.rename(from, to), new rxjs_1.Observable((x) => { this._filesToOverwrite.add(to); x.complete(); })); } if (this._filesToDelete.has(to)) { this._filesToDelete.delete(to); this._filesToDelete.add(from); this._filesToOverwrite.add(to); // We need to delete the original and write the new one. return this.read(from).pipe((0, rxjs_1.map)((content) => this._write(to, content))); } const maybeTo1 = this._filesToRenameRevert.get(from); if (maybeTo1) { // We already renamed to this file (A => from), let's rename the former to the new // path (A => to). this._filesToRename.delete(maybeTo1); this._filesToRenameRevert.delete(from); from = maybeTo1; } this._filesToRename.set(from, to); this._filesToRenameRevert.set(to, from); // If the file is part of our data, just rename it internally. if (this._exists(from)) { return super.rename(from, to); } else { // Create a file with the same content. return this._back.read(from).pipe((0, rxjs_1.switchMap)((content) => super.write(to, content))); } })); } list(path) { return (0, rxjs_1.concat)(super.list(path), this._back.list(path)).pipe((0, rxjs_1.reduce)((list, curr) => { curr.forEach((elem) => list.add(elem)); return list; }, new Set()), (0, rxjs_1.map)((set) => [...set])); } exists(path) { return this._exists(path) ? (0, rxjs_1.of)(true) : this.willDelete(path) || this.willRename(path) ? (0, rxjs_1.of)(false) : this._back.exists(path); } isDirectory(path) { return this._exists(path) ? super.isDirectory(path) : this._back.isDirectory(path); } isFile(path) { return this._exists(path) ? super.isFile(path) : this.willDelete(path) || this.willRename(path) ? (0, rxjs_1.of)(false) : this._back.isFile(path); } stat(path) { return this._exists(path) ? super.stat(path) : this.willDelete(path) || this.willRename(path) ? (0, rxjs_1.of)(null) : this._back.stat(path); } watch(path, options) { // Watching not supported. return null; } willCreate(path) { return this._filesToCreate.has(path); } willOverwrite(path) { return this._filesToOverwrite.has(path); } willDelete(path) { return this._filesToDelete.has(path); } willRename(path) { return this._filesToRename.has(path); } willRenameTo(path, to) { return this._filesToRename.get(path) === to; } } exports.CordHost = CordHost;