/** * @license * Copyright Google Inc. 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 { PathLike, Stats, constants, existsSync, mkdirSync, promises as fsPromises, readFileSync, readdirSync, renameSync, rmdirSync, statSync, unlinkSync, writeFileSync, } from 'fs'; import { dirname as pathDirname, join as pathJoin } 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'; interface ChokidarWatcher { new(options: {}): ChokidarWatcher; add(path: string): ChokidarWatcher; on(type: 'change', cb: (path: string) => void): ChokidarWatcher; on(type: 'add', cb: (path: string) => void): ChokidarWatcher; on(type: 'unlink', cb: (path: string) => void): ChokidarWatcher; close(): void; } async function exists(path: PathLike): Promise { 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: ChokidarWatcher; function loadFSWatcher() { if (!FSWatcher) { try { // tslint:disable-next-line:no-implicit-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 { get capabilities(): virtualFs.HostCapabilities { return { synchronous: false }; } write(path: Path, content: virtualFs.FileBuffer): Observable { return observableFrom(fsPromises.mkdir(getSystemPath(dirname(path)), { recursive: true })) .pipe( mergeMap(() => fsPromises.writeFile(getSystemPath(path), content)), ); } read(path: Path): Observable { return observableFrom(fsPromises.readFile(getSystemPath(path))) .pipe( map(buffer => new Uint8Array(buffer).buffer as virtualFs.FileBuffer), ); } delete(path: Path): Observable { return this.isDirectory(path).pipe( mergeMap(async isDirectory => { if (isDirectory) { const recursiveDelete = async (dirPath: string) => { for (const fragment of (await fsPromises.readdir(dirPath))) { const sysPath = pathJoin(dirPath, fragment); const stats = await fsPromises.stat(sysPath); if (stats.isDirectory()) { await recursiveDelete(sysPath); await fsPromises.rmdir(sysPath); } else { await fsPromises.unlink(sysPath); } } }; await recursiveDelete(getSystemPath(path)); } else { await fsPromises.unlink(getSystemPath(path)); } }), ); } rename(from: Path, to: Path): Observable { return observableFrom(fsPromises.rename(getSystemPath(from), getSystemPath(to))); } list(path: Path): Observable { return observableFrom(fsPromises.readdir(getSystemPath(path))).pipe( map((names) => names.map(name => fragment(name))), ); } exists(path: Path): Observable { return observableFrom(exists(path)); } isDirectory(path: Path): Observable { return this.stat(path).pipe( map(stat => stat.isDirectory()), ); } isFile(path: Path): Observable { return this.stat(path).pipe( map(stat => stat.isFile()), ); } // Some hosts may not support stat. stat(path: Path): Observable> { return observableFrom(fsPromises.stat(getSystemPath(path))); } // Some hosts may not support watching. watch( path: Path, _options?: virtualFs.HostWatchOptions, ): Observable | null { return new Observable(obs => { loadFSWatcher(); const watcher = new FSWatcher({ persistent: true }).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 { get capabilities(): virtualFs.HostCapabilities { return { synchronous: true }; } write(path: Path, content: virtualFs.FileBuffer): Observable { return new Observable(obs => { mkdirSync(getSystemPath(dirname(path)), { recursive: true }); writeFileSync(getSystemPath(path), new Uint8Array(content)); obs.next(); obs.complete(); }); } read(path: Path): Observable { return new Observable(obs => { const buffer = readFileSync(getSystemPath(path)); obs.next(new Uint8Array(buffer).buffer as virtualFs.FileBuffer); obs.complete(); }); } delete(path: Path): Observable { return this.isDirectory(path).pipe( concatMap(isDir => { if (isDir) { const dirPaths = readdirSync(getSystemPath(path)); const rmDirComplete = new Observable((obs) => { rmdirSync(getSystemPath(path)); 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 { 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 { return new Observable(obs => { const names = readdirSync(getSystemPath(path)); obs.next(names.map(name => fragment(name))); obs.complete(); }); } exists(path: Path): Observable { return new Observable(obs => { obs.next(existsSync(getSystemPath(path))); obs.complete(); }); } isDirectory(path: Path): Observable { // tslint:disable-next-line:no-non-null-assertion return this.stat(path)!.pipe(map(stat => stat.isDirectory())); } isFile(path: Path): Observable { // tslint:disable-next-line:no-non-null-assertion return this.stat(path)!.pipe(map(stat => stat.isFile())); } // Some hosts may not support stat. stat(path: Path): Observable> { return new Observable(obs => { obs.next(statSync(getSystemPath(path))); obs.complete(); }); } // Some hosts may not support watching. watch( path: Path, _options?: virtualFs.HostWatchOptions, ): Observable | null { return new Observable(obs => { const opts = { persistent: false }; loadFSWatcher(); const watcher = new FSWatcher(opts).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(), ); } }