import { promisify } from 'util' ; import * as path from 'path' ; import * as _fs from 'fs' ; import * as cp from 'child_process' ; namespace fs { export const exist = promisify ( _fs . exists ) ; export const readFile = promisify ( _fs . readFile ) ; export const stat = promisify ( _fs . stat ) ; export const Stats = _fs . Stats ; } namespace pcp { export const exec = promisify ( cp . exec ) ; } import latestVersion from 'latest-version' ; interface Dictionary < T > { [ key ] : T ; } interface PackageJson { devDependencies : Dictionary < string > ; dependencies : Dictionary < string > ; } function stripComments ( content ) { const regexp = /("(?:[^\\"]*(?:\\.)?)*")|('(?:[^\\']*(?:\\.)?)*')|(\/\*(?:\r?\n|.)*?\*\/)|(\/{2,}.*?(?:(?:\r?\n)|$))/g ; return content . replace ( regexp , function ( match , _m1 , _m2 , m3 , m4 ) { if ( m3 ) { return '' ; } else if ( m4 ) { const length_1 = m4 . length ; if ( length_1 > 2 && m4 [ length_1 - 1 ] === '\n' ) { return m4 [ length_1 - 2 ] === '\r' ? '\r\n' : '\n' ; } else { return '' ; } } else { return match ; } } ) ; } export class TypingsInstaller { private handledPackages : Set < string > ; private handledTsConfig : Set < string > ; constructor ( ) { this . handledPackages = new Set ( ) ; this . handledTsConfig = new Set ( ) ; } private static ensureSeparator ( directory ) { return directory [ directory . length - 1 ] !== path . sep ? `${ directory }${ path . sep }` : directory ; } public async installTypings ( projectRoot , start , typings ) { if ( typings . length === 0 ) { return ; } let stat = await fs . stat ( start ) ; let startDirectory ; let key ; if ( stat . isDirectory ( ) ) { startDirectory = start ; key = path . join ( start , typings . join ( ':' ) ) ; } else if ( stat . isFile ( ) ) { startDirectory = path . dirname ( start ) ; key = start ; } else { return ; } if ( this . handledTsConfig . has ( key ) ) { return ; } if ( startDirectory . length < projectRoot . length ) { return ; } projectRoot = path . normalize ( projectRoot ) ; typings = typings . map ( typing => typing . startsWith ( '@types/' ) ? typing : `@types/${ typing }` ) ; while ( startDirectory . length >= projectRoot . length ) { let packageFile = path . join ( startDirectory , 'package.json' ) ; if ( await fs . exist ( packageFile ) ) { typings = await this . filterTypingsToInstall ( packageFile , typings ) ; if ( typings . length === 0 ) { return ; } await this . doInstallTypingsFromNpm ( await this . validateTypingsOnNpm ( typings ) ) ; this . handledTsConfig . add ( key ) ; return ; } startDirectory = path . dirname ( startDirectory ) ; } } public async guessTypings ( projectRoot , startDirectory ) { if ( startDirectory . length < projectRoot . length ) { return ; } projectRoot = path . normalize ( projectRoot ) ; startDirectory = path . normalize ( startDirectory ) ; if ( ! TypingsInstaller . ensureSeparator ( startDirectory ) . startsWith ( TypingsInstaller . ensureSeparator ( projectRoot ) ) ) { return ; } while ( startDirectory . length >= projectRoot . length ) { let packageFile = path . join ( startDirectory , 'package.json' ) ; if ( this . handledPackages . has ( packageFile ) ) { return ; } if ( await fs . exist ( packageFile ) ) { let typings = await this . findTypingsToInstall ( packageFile ) ; if ( typings . length === 0 ) { continue ; } await this . doInstallTypingsFromNpm ( await this . validateTypingsOnNpm ( typings ) ) ; this . handledPackages . add ( packageFile ) ; } startDirectory = path . dirname ( startDirectory ) ; } } private async findTypingsToInstall ( packageFile ) { const typings = new Set ( ) ; const toInstall = [ ] ; const packageJson = JSON . parse ( stripComments ( await fs . readFile ( packageFile , 'utf8' ) ) ) ; if ( packageJson . devDependencies ) { for ( let pack of Object . keys ( packageJson . devDependencies ) ) { if ( pack . startsWith ( '@types/' ) ) { typings . add ( pack ) ; } } } if ( packageJson . dependencies !== undefined ) { for ( let pack of Object . keys ( packageJson . dependencies ) ) { if ( pack . startsWith ( '@types/' ) ) { typings . add ( pack ) ; } } for ( let pack of Object . keys ( packageJson . dependencies ) ) { if ( pack . startsWith ( '@types/' ) ) { continue ; } const typing = `@types/${ pack }` ; if ( ! typings . has ( typing ) ) { toInstall . push ( typing ) ; } } } return toInstall ; } private async filterTypingsToInstall ( packageFile , toInstall ) { const typings = new Set ( ) ; const packageJson = JSON . parse ( stripComments ( await fs . readFile ( packageFile , 'utf8' ) ) ) ; if ( packageJson . devDependencies ) { for ( let pack of Object . keys ( packageJson . devDependencies ) ) { if ( pack . startsWith ( '@types/' ) ) { typings . add ( pack ) ; } } } if ( packageJson . dependencies !== undefined ) { for ( let pack of Object . keys ( packageJson . dependencies ) ) { if ( pack . startsWith ( '@types/' ) ) { typings . add ( pack ) ; } } } let result = [ ] ; for ( let typing of toInstall ) { if ( ! typings . has ( typing ) ) { result . push ( typing ) ; } } return result ; } private async validateTypingsOnNpm ( typings ) { if ( typings . length === 0 ) { return typings ; } const promises = [ ] ; for ( let typing of typings ) { try { promises . push ( latestVersion ( typing ) . then ( ( ) => typing , ( _error ) => undefined ) ) ; } catch ( error ) { } } const all = await Promise . all ( promises ) ; const result = [ ] ; for ( let elem of all ) { if ( elem !== undefined ) { result . push ( elem ) ; } } return result ; } private async doInstallTypingsFromNpm ( typings ) { if ( typings . length === 0 ) { return ; } const command = `npm install --no-save ${ typings . join ( ' ' ) }` ; await pcp . exec ( command ) ; } }