@angular-devkit/core
Version:
Angular DevKit - Core Utility Library
315 lines (314 loc) • 12.4 kB
JavaScript
"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;