/* Copyright IBM Corp. 2017 */ import { join, normalize, parse } from 'path'; import { from, Observable, of, throwError, UnaryFunction } from 'rxjs'; import { catchError, concatMap, distinct, filter, map, mergeMap, shareReplay, startWith, toArray } from 'rxjs/operators'; import { rxCopyFiles } from './rx.copy'; import { rxReadTextFile } from './rx.file'; import { FileDescriptor, rxStats, rxWalkFiles } from './rx.walk'; const toposort: UnaryFunction = require('toposort'); export const KEY_WCHTOOLS_DEPENDENCIES = 'wchtools-dependencies'; export const DEPENDENCIES = 'dependencies'; interface Cycle { [module: string]: Observable; } function _resolveToPackage(aModule: string, aRootPath: string): Observable { // check for the existence of a node-modules folder const normRoot = normalize(aRootPath); const pkgName = normalize(join(normRoot, 'node_modules', aModule, 'package.json')); const normParent = normalize(join(normRoot, '..')); // check if this is a folder const rxHasPackageJson = rxStats(pkgName).pipe( catchError(() => of(undefined)), map(stat => stat && stat.isFile()) ); // test return rxHasPackageJson.pipe( mergeMap(bExists => bExists ? of(pkgName) : (normRoot === normParent) ? throwError(aModule) : _resolveToPackage(aModule, normParent))); } function _isRelativePath(aPath: string): boolean { return aPath.startsWith('./') || aPath.startsWith('../'); } function _resolveEdges(aPackageFile: string, aCycle: Cycle): Observable { // load the package file const rxPackage = rxReadTextFile(aPackageFile).pipe( map(data => JSON.parse(data)) ); // extract the dependencies const rxDeps = rxPackage.pipe( mergeMap(pkg => from((pkg[KEY_WCHTOOLS_DEPENDENCIES] || Object.keys(pkg[DEPENDENCIES] || {})) as string[])) ); // work on the dependencies and resolve the modules return rxDeps.pipe( mergeMap(dep => { // root path const parsed = parse(aPackageFile); // handle relative paths if (_isRelativePath(dep)) { // just resolve return of([aPackageFile, normalize(join(parsed.dir, dep))]); } // check of a cycle on the module name let rxCycle = aCycle[dep]; if (rxCycle) { // break cycle return rxCycle.pipe( map(pkg => [aPackageFile, pkg]) ); } // register the module rxCycle = aCycle[dep] = _resolveToPackage(dep, parsed.dir).pipe( shareReplay(1) ); // resolve dependency return rxCycle.pipe( mergeMap(pkg => _resolveEdges(pkg, aCycle).pipe( startWith([aPackageFile, pkg])) ) ); }) ); } function _isPackageJson(aPath: string): boolean { const parsed = parse(aPath); return parsed.base === 'package.json'; } export function rxResolveEdges(aPackageFile: string): Observable { return _resolveEdges(aPackageFile, {}); } /** * Returns the 'wchtools-dependencies' in topological order * * @param aPackageFile the package file * @return the dependencies in topological order */ export function rxResolveDependencies(aPackageFile: string, aReversedOrder?: boolean): Observable { return rxResolveEdges(aPackageFile).pipe( toArray(), map(toposort), map((res: string[]) => aReversedOrder ? res : res.reverse()), mergeMap(res => from(res)), filter(res => !_isPackageJson(res)) ); } function _distinctKey(aDesc: FileDescriptor): string { return aDesc.relPath; } /** * Provides a listing of all files to copie copied, in overlayed order. The relative * paths of the result are the relevant target paths * * @param aPackageFile the package file * @return observable of the copied files */ export function rxAggregateDependencies(aPackageFile: string): Observable { // walk over the dependencies return rxResolveDependencies(aPackageFile, true).pipe( concatMap(rxWalkFiles), distinct(_distinctKey) ); } /** * Provides a listing of all files to copie copied, in overlayed order. The relative * paths of the result are the relevant target paths * * @param aPackageFile the package file * @param aDstDir target directory * @return observable of the copied files */ export function rxCopyDependencies(aPackageFile: string, aDstDir: string): Observable { // walk over the dependencies return rxCopyFiles(rxAggregateDependencies(aPackageFile), aDstDir); }