Alan Agius a169f26513 refactor(@angular-devkit/core): use chokidar types instead of custom interface
Replace `ChokidarWatcher` with types from `chokidar`
2021-10-09 07:04:18 +02:00

332 lines
9.5 KiB
TypeScript

/**
* @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.io/license
*/
import type { FSWatcher as ChokidarWatcher } from 'chokidar';
import fs, {
PathLike,
Stats,
constants,
existsSync,
promises as fsPromises,
mkdirSync,
readFileSync,
readdirSync,
renameSync,
statSync,
unlinkSync,
writeFileSync,
} from 'fs';
import { dirname as pathDirname } from 'path';
import { Observable, concat, from as observableFrom, of, throwError } from 'rxjs';
import { concatMap, map, mergeMap, publish, refCount } from 'rxjs/operators';
import {
Path,
PathFragment,
dirname,
fragment,
getSystemPath,
join,
normalize,
virtualFs,
} from '../src';
async function exists(path: PathLike): Promise<boolean> {
try {
await fsPromises.access(path, constants.F_OK);
return true;
} catch {
return false;
}
}
// This will only be initialized if the watch() method is called.
// Otherwise chokidar appears only in type positions, and shouldn't be referenced
// in the JavaScript output.
let FSWatcher: typeof ChokidarWatcher;
function loadFSWatcher() {
if (!FSWatcher) {
try {
// eslint-disable-next-line import/no-extraneous-dependencies
FSWatcher = require('chokidar').FSWatcher;
} catch (e) {
if (e.code !== 'MODULE_NOT_FOUND') {
throw new Error(
'As of angular-devkit version 8.0, the "chokidar" package ' +
'must be installed in order to use watch() features.',
);
}
throw e;
}
}
}
/**
* An implementation of the Virtual FS using Node as the background. There are two versions; one
* synchronous and one asynchronous.
*/
export class NodeJsAsyncHost implements virtualFs.Host<Stats> {
get capabilities(): virtualFs.HostCapabilities {
return { synchronous: false };
}
write(path: Path, content: virtualFs.FileBuffer): Observable<void> {
return observableFrom(fsPromises.mkdir(getSystemPath(dirname(path)), { recursive: true })).pipe(
mergeMap(() => fsPromises.writeFile(getSystemPath(path), new Uint8Array(content))),
);
}
read(path: Path): Observable<virtualFs.FileBuffer> {
return observableFrom(fsPromises.readFile(getSystemPath(path))).pipe(
map((buffer) => new Uint8Array(buffer).buffer as virtualFs.FileBuffer),
);
}
delete(path: Path): Observable<void> {
return this.isDirectory(path).pipe(
mergeMap(async (isDirectory) => {
if (isDirectory) {
// The below should be removed and replaced with just `rm` when support for Node.Js 12 is removed.
const { rm, rmdir } = fsPromises as typeof fsPromises & {
rm?: (
path: fs.PathLike,
options?: {
force?: boolean;
maxRetries?: number;
recursive?: boolean;
retryDelay?: number;
},
) => Promise<void>;
};
if (rm) {
await rm(getSystemPath(path), { force: true, recursive: true, maxRetries: 3 });
} else {
await rmdir(getSystemPath(path), { recursive: true, maxRetries: 3 });
}
} else {
await fsPromises.unlink(getSystemPath(path));
}
}),
);
}
rename(from: Path, to: Path): Observable<void> {
return observableFrom(fsPromises.rename(getSystemPath(from), getSystemPath(to)));
}
list(path: Path): Observable<PathFragment[]> {
return observableFrom(fsPromises.readdir(getSystemPath(path))).pipe(
map((names) => names.map((name) => fragment(name))),
);
}
exists(path: Path): Observable<boolean> {
return observableFrom(exists(getSystemPath(path)));
}
isDirectory(path: Path): Observable<boolean> {
return this.stat(path).pipe(map((stat) => stat.isDirectory()));
}
isFile(path: Path): Observable<boolean> {
return this.stat(path).pipe(map((stat) => stat.isFile()));
}
// Some hosts may not support stat.
stat(path: Path): Observable<virtualFs.Stats<Stats>> {
return observableFrom(fsPromises.stat(getSystemPath(path)));
}
// Some hosts may not support watching.
watch(
path: Path,
_options?: virtualFs.HostWatchOptions,
): Observable<virtualFs.HostWatchEvent> | null {
return new Observable<virtualFs.HostWatchEvent>((obs) => {
loadFSWatcher();
const watcher = new FSWatcher({ persistent: true });
watcher.add(getSystemPath(path));
watcher
.on('change', (path) => {
obs.next({
path: normalize(path),
time: new Date(),
type: virtualFs.HostWatchEventType.Changed,
});
})
.on('add', (path) => {
obs.next({
path: normalize(path),
time: new Date(),
type: virtualFs.HostWatchEventType.Created,
});
})
.on('unlink', (path) => {
obs.next({
path: normalize(path),
time: new Date(),
type: virtualFs.HostWatchEventType.Deleted,
});
});
return () => watcher.close();
}).pipe(publish(), refCount());
}
}
/**
* An implementation of the Virtual FS using Node as the backend, synchronously.
*/
export class NodeJsSyncHost implements virtualFs.Host<Stats> {
get capabilities(): virtualFs.HostCapabilities {
return { synchronous: true };
}
write(path: Path, content: virtualFs.FileBuffer): Observable<void> {
return new Observable((obs) => {
mkdirSync(getSystemPath(dirname(path)), { recursive: true });
writeFileSync(getSystemPath(path), new Uint8Array(content));
obs.next();
obs.complete();
});
}
read(path: Path): Observable<virtualFs.FileBuffer> {
return new Observable((obs) => {
const buffer = readFileSync(getSystemPath(path));
obs.next(new Uint8Array(buffer).buffer as virtualFs.FileBuffer);
obs.complete();
});
}
delete(path: Path): Observable<void> {
return this.isDirectory(path).pipe(
concatMap((isDir) => {
if (isDir) {
const dirPaths = readdirSync(getSystemPath(path));
const rmDirComplete = new Observable<void>((obs) => {
// The below should be removed and replaced with just `rmSync` when support for Node.Js 12 is removed.
const { rmSync, rmdirSync } = fs as typeof fs & {
rmSync?: (
path: fs.PathLike,
options?: {
force?: boolean;
maxRetries?: number;
recursive?: boolean;
retryDelay?: number;
},
) => void;
};
if (rmSync) {
rmSync(getSystemPath(path), { force: true, recursive: true, maxRetries: 3 });
} else {
rmdirSync(getSystemPath(path), { recursive: true, maxRetries: 3 });
}
obs.complete();
});
return concat(...dirPaths.map((name) => this.delete(join(path, name))), rmDirComplete);
} else {
try {
unlinkSync(getSystemPath(path));
} catch (err) {
return throwError(err);
}
return of(undefined);
}
}),
);
}
rename(from: Path, to: Path): Observable<void> {
return new Observable((obs) => {
const toSystemPath = getSystemPath(to);
mkdirSync(pathDirname(toSystemPath), { recursive: true });
renameSync(getSystemPath(from), toSystemPath);
obs.next();
obs.complete();
});
}
list(path: Path): Observable<PathFragment[]> {
return new Observable((obs) => {
const names = readdirSync(getSystemPath(path));
obs.next(names.map((name) => fragment(name)));
obs.complete();
});
}
exists(path: Path): Observable<boolean> {
return new Observable((obs) => {
obs.next(existsSync(getSystemPath(path)));
obs.complete();
});
}
isDirectory(path: Path): Observable<boolean> {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return this.stat(path)!.pipe(map((stat) => stat.isDirectory()));
}
isFile(path: Path): Observable<boolean> {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return this.stat(path)!.pipe(map((stat) => stat.isFile()));
}
// Some hosts may not support stat.
stat(path: Path): Observable<virtualFs.Stats<Stats>> {
return new Observable((obs) => {
obs.next(statSync(getSystemPath(path)));
obs.complete();
});
}
// Some hosts may not support watching.
watch(
path: Path,
_options?: virtualFs.HostWatchOptions,
): Observable<virtualFs.HostWatchEvent> | null {
return new Observable<virtualFs.HostWatchEvent>((obs) => {
loadFSWatcher();
const watcher = new FSWatcher({ persistent: false });
watcher.add(getSystemPath(path));
watcher
.on('change', (path) => {
obs.next({
path: normalize(path),
time: new Date(),
type: virtualFs.HostWatchEventType.Changed,
});
})
.on('add', (path) => {
obs.next({
path: normalize(path),
time: new Date(),
type: virtualFs.HostWatchEventType.Created,
});
})
.on('unlink', (path) => {
obs.next({
path: normalize(path),
time: new Date(),
type: virtualFs.HostWatchEventType.Deleted,
});
});
return () => watcher.close();
}).pipe(publish(), refCount());
}
}