import * as ts from 'typescript' ; import * as fs from 'fs' ; import * as path from 'path' ; import fg = require ( 'fast-glob' ) ; import normalize = require ( 'normalize-path' ) ; public export async function getProjectRootNamesAndCompilerOptions ( project ) { const { configFilePath , dirname } = getTsConfigFilePath ( project ) ; const config = await getTsConfig ( configFilePath , dirname ) ; const { options : compilerOptions , errors } = ts . convertCompilerOptionsFromJson ( config . compilerOptions , config . basePath || dirname ) ; if ( errors && errors . length > 0 ) { throw errors ; } const rootNames = await getRootNames ( config , dirname ) ; return { rootNames , compilerOptions } ; } function tryToStatFile ( filePath ) { try { return { path : filePath , stats : fs . statSync ( filePath ) , } ; } catch { try { filePath = filePath + '.json' ; return { path : filePath , stats : fs . statSync ( filePath ) , } ; } catch { return undefined ; } } } function getTsConfigFilePath ( project , fallbackProject ? ) { let configFilePath ; let dirname ; let projectStats ; let result = tryToStatFile ( project ) ; if ( result ) { project = result . path ; projectStats = result . stats ; } else if ( fallbackProject ) { while ( fallbackProject . length > 0 ) { result = tryToStatFile ( fallbackProject [ 0 ] ! ) ; if ( result ) { project = result . path ; projectStats = result . stats ; break ; } else { fallbackProject . shift ( ) ; } } } if ( projectStats && projectStats . isDirectory ( ) ) { configFilePath = path . resolve ( project , 'tsconfig.json' ) ; dirname = project ; } else if ( projectStats && projectStats . isFile ( ) ) { configFilePath = project ; dirname = path . dirname ( project ) ; } else { throw new Error ( "paramter 'project' should be a file or directory." ) ; } return { configFilePath , dirname } ; } interface JsonConfig { extends ? : string ; compilerOptions ? : { baseUrl ? : string ; outDir ? : string ; [ name ] : unknown ; } ; include ? : string [ ] ; exclude ? : string [ ] ; files ? : string [ ] ; basePath ? : string ; } async function getTsConfig ( configFilePath , dirname ) { const configResult = ts . readConfigFile ( configFilePath , p => fs . readFileSync ( p ) . toString ( ) ) ; const config = configResult . error ? { extends : undefined , compilerOptions : { lib : [ 'dom' , 'es5' , 'es2015' , 'es2016' , 'es2017' ] , allowSyntheticDefaultImports : true } } : configResult . config as JsonConfig ; if ( config . extends ) { let project ; let fallbackProjects = [ ] ; if ( path . isAbsolute ( config . extends ) ) { project = config . extends ; } else if ( config . extends === '.' || config . extends === '..' || config . extends . startsWith ( `.${ path . sep }` ) || config . extends . startsWith ( `..${ path . sep }` ) || config . extends . startsWith ( './' ) || config . extends . startsWith ( '../' ) ) { project = path . resolve ( dirname , config . extends ) ; } else { project = path . resolve ( dirname , 'node_modules' , config . extends ) ; const paths = await findParentsWithNodeModules ( dirname ) ; fallbackProjects = paths . map ( p => path . resolve ( p , 'node_modules' , config . extends || '' ) ) ; } const { configFilePath , dirname : extendsBasename } = getTsConfigFilePath ( project , fallbackProjects ) ; const extendsConfig = await getTsConfig ( configFilePath , extendsBasename ) ; const topLevelBaseUrl = config . compilerOptions ? config . compilerOptions . baseUrl : undefined ; config . compilerOptions = { ... extendsConfig . compilerOptions , ... config . compilerOptions } ; config . basePath = topLevelBaseUrl ? dirname : extendsBasename ; } return config ; } async function getRootNames ( config , dirname ) { let include ; if ( config . include ) { include = config . include ; } else { include = config . files ? [ ] : [ '**/*' ] ; } const files = config . files ?. map ( f => path . resolve ( dirname , f ) ) ?? [ ] ; if ( Array . isArray ( include ) && include . length > 0 ) { let exclude ; if ( config . exclude ) { exclude = config . exclude ; } else { exclude = [ 'node_modules' , 'bower_components' , 'jspm_packages' ] ; if ( config . compilerOptions ?. outDir ) { exclude . push ( config . compilerOptions . outDir ) ; } } let ignore = [ ] ; for ( const e of exclude ) { ignore . push ( e , `**/${ e }` ) ; } let rules = [ ] ; for ( const file of include ) { const currentPath = path . resolve ( dirname , file ) ; const stats = await statAsync ( currentPath ) ; if ( stats === undefined || stats . isFile ( ) ) { rules . push ( currentPath ) ; } else if ( stats . isDirectory ( ) ) { rules . push ( `${ currentPath . endsWith ( '/' ) ? currentPath . substring ( 0 , currentPath . length - 1 ) : currentPath }/**/*` ) ; } } rules = rules . map ( ( r ) => normalize ( r ) ) ; ignore = ignore . map ( ( r ) => normalize ( r ) ) ; const includeFiles = await fg ( rules , { ignore , cwd : dirname , } ) ; files . push ( ... includeFiles ) ; } return files . map ( ( r ) => path . resolve ( process . cwd ( ) , dirname , r ) ) ; } function statAsync ( file ) { return new Promise < fs . Stats | undefined > ( ( resolve ) => { fs . stat ( file , ( error , stats ) => { if ( error ) { resolve ( undefined ) ; } else { resolve ( stats ) ; } } ) ; } ) ; } async function findParentsWithNodeModules ( dir ) { const result = [ process . cwd ( ) ] ; dir = path . resolve ( dir ) ; for ( let i = 0 ; i < 3 ; i ++ ) { dir = path . dirname ( dir ) ; const stats = await statAsync ( path . resolve ( dir , 'node_modules' ) ) ; if ( stats && stats . isDirectory ( ) && ! result . includes ( dir ) ) { result . push ( dir ) ; } } return result ; }