import * as fs from 'fs' ; import * as path from 'path' ; import * as tsickle from 'tsickle' ; import * as ts from 'typescript' ; import { Plugin as BazelConformancePlugin } from '../tsetse/runner' ; import { CachedFileLoader , FileLoader , ProgramAndFileCache , UncachedFileLoader } from './cache' ; import { CompilerHost } from './compiler_host' ; import * as bazelDiagnostics from './diagnostics' ; import { constructManifest } from './manifest' ; import * as perfTrace from './perf_trace' ; import { DiagnosticPlugin , PluginCompilerHost , EmitPlugin } from './plugin_api' ; import { Plugin as StrictDepsPlugin } from './strict_deps' ; import { BazelOptions , parseTsconfig , resolveNormalizedPath } from './tsconfig' ; import { debug , log , runAsWorker , runWorkerLoop } from './worker' ; /**
 * Top-level entry point for tsc_wrapped.
 */ export function main ( args ) { if ( runAsWorker ( args ) ) { log ( 'Starting TypeScript compiler persistent worker...' ) ; runWorkerLoop ( runOneBuild ) ; } else { debug ( 'Running a single build...' ) ; if ( args . length === 0 ) throw new Error ( 'Not enough arguments' ) ; if ( ! runOneBuild ( args ) ) { return 1 ; } } return 0 ; } /** The one ProgramAndFileCache instance used in this process. */ const cache = new ProgramAndFileCache ( debug ) ; function isCompilationTarget ( bazelOpts , sf ) { if ( bazelOpts . isJsTranspilation && bazelOpts . transpiledJsInputDirectory ) { return sf . fileName . startsWith ( bazelOpts . compilationTargetSrc [ 0 ] ) ; } return ( bazelOpts . compilationTargetSrc . indexOf ( sf . fileName ) !== - 1 ) ; } /**
 * Gather diagnostics from TypeScript's type-checker as well as other plugins we
 * install such as strict dependency checking.
 */ export function gatherDiagnostics ( options , bazelOpts , program , disabledTsetseRules , plugins = [ ] , ignoreForDiagnostics = new Set ( ) ) { const diagnostics = [ ] ; perfTrace . wrap ( 'type checking' , ( ) => { if ( ! bazelOpts . typeCheck ) { return ; } perfTrace . wrap ( `global diagnostics` , ( ) => { diagnostics . push ( ... program . getOptionsDiagnostics ( ) ) ; diagnostics . push ( ... program . getGlobalDiagnostics ( ) ) ; } ) ; let isTypeCheckTarget = ( f ) => bazelOpts . typeCheckDependencies || isCompilationTarget ( bazelOpts , f ) ; let sourceFilesToCheck = program . getSourceFiles ( ) . filter ( f => isTypeCheckTarget ( f ) && ! ignoreForDiagnostics . has ( f ) ) ; for ( const sf of sourceFilesToCheck ) { perfTrace . wrap ( `check ${ sf . fileName }` , ( ) => { diagnostics . push ( ... program . getSyntacticDiagnostics ( sf ) ) ; diagnostics . push ( ... program . getSemanticDiagnostics ( sf ) ) ; } ) ; perfTrace . snapshotMemoryUsage ( ) ; } plugins . push ( ... getCommonPlugins ( options , bazelOpts , program , disabledTsetseRules ) ) ; for ( const plugin of plugins ) { perfTrace . wrap ( `${ plugin . name } diagnostics` , ( ) => { for ( const sf of sourceFilesToCheck ) { perfTrace . wrap ( `${ plugin . name } checking ${ sf . fileName }` , ( ) => { const pluginDiagnostics = plugin . getDiagnostics ( sf ) . map ( ( d ) => { return tagDiagnosticWithPlugin ( plugin . name , d ) ; } ) ; diagnostics . push ( ... pluginDiagnostics ) ; } ) ; perfTrace . snapshotMemoryUsage ( ) ; } } ) ; } } ) ; return diagnostics ; } /**
 * Construct diagnostic plugins that we always want included.
 *
 * TODO: Call sites of getDiagnostics should initialize plugins themselves,
 *   including these, and the arguments to getDiagnostics should be simplified.
 */ export function getCommonPlugins ( options , bazelOpts , program , disabledTsetseRules ) { const plugins = [ ] ; if ( ! bazelOpts . disableStrictDeps ) { if ( options . rootDir == null ) { throw new Error ( `StrictDepsPlugin requires that rootDir be specified` ) ; } plugins . push ( new StrictDepsPlugin ( program , { ... bazelOpts , rootDir : options . rootDir } ) ) ; } if ( ! bazelOpts . isJsTranspilation ) { let tsetsePluginConstructor = BazelConformancePlugin ; plugins . push ( new tsetsePluginConstructor ( program , disabledTsetseRules ) ) ; } return plugins ; } /**
 * Returns a copy of diagnostic with one whose text has been prepended with
 * an indication of what plugin contributed that diagnostic.
 *
 * This is slightly complicated because a diagnostic's message text can be
 * split up into a chain of diagnostics, e.g. when there's supplementary info
 * about a diagnostic.
 */ function tagDiagnosticWithPlugin ( pluginName , diagnostic ) { const tagMessageWithPluginName = ( text ) => `[${ pluginName }] ${ text }` ; let messageText ; if ( typeof diagnostic . messageText === 'string' ) { messageText = tagMessageWithPluginName ( diagnostic . messageText ) ; } else { const chain = diagnostic . messageText ; messageText = { ... chain , messageText : tagMessageWithPluginName ( chain . messageText ) } ; } return { ... diagnostic , messageText } ; } /**
 * expandSourcesFromDirectories finds any directories under filePath and expands
 * them to their .js or .ts contents.
 */ function expandSourcesFromDirectories ( fileList , filePath ) { if ( filePath . endsWith ( '.ts' ) || filePath . endsWith ( '.tsx' ) || filePath . endsWith ( '.js' ) ) { fileList . push ( filePath ) ; return ; } if ( ! fs . statSync ( filePath ) . isDirectory ( ) ) { return ; } const entries = fs . readdirSync ( filePath ) ; for ( const entry of entries ) { expandSourcesFromDirectories ( fileList , path . join ( filePath , entry ) ) ; } } /**
 * Runs a single build, returning false on failure.  This is potentially called
 * multiple times (once per bazel request) when running as a bazel worker.
 * Any encountered errors are written to stderr.
 */ function runOneBuild ( args , inputs ? ) { if ( args . length !== 1 ) { console . error ( 'Expected one argument: path to tsconfig.json' ) ; return false ; } perfTrace . snapshotMemoryUsage ( ) ; const tsconfigFile = args [ 0 ] . replace ( /^@+/ , '' ) ; const [ parsed , errors , { target } ] = parseTsconfig ( tsconfigFile ) ; if ( errors ) { console . error ( bazelDiagnostics . format ( target , errors ) ) ; return false ; } if ( ! parsed ) { throw new Error ( 'Impossible state: if parseTsconfig returns no errors, then parsed should be non-null' ) ; } const { options , bazelOpts , files , disabledTsetseRules } = parsed ; let sourceFiles = [ ] ; if ( bazelOpts . isJsTranspilation ) { for ( const filePath of files ) { expandSourcesFromDirectories ( sourceFiles , filePath ) ; } } else { sourceFiles = files ; } if ( bazelOpts . maxCacheSizeMb !== undefined ) { const maxCacheSizeBytes = bazelOpts . maxCacheSizeMb * ( 1 << 20 ) ; cache . setMaxCacheSize ( maxCacheSizeBytes ) ; } else { cache . resetMaxCacheSize ( ) ; } let fileLoader ; if ( inputs ) { fileLoader = new CachedFileLoader ( cache ) ; const resolvedInputs = new Map < string , string > ( ) ; for ( const key of Object . keys ( inputs ) ) { resolvedInputs . set ( resolveNormalizedPath ( key ) , inputs [ key ] ) ; } cache . updateCache ( resolvedInputs ) ; } else { fileLoader = new UncachedFileLoader ( ) ; } const diagnostics = perfTrace . wrap ( 'createProgramAndEmit' , ( ) => { return createProgramAndEmit ( fileLoader , options , bazelOpts , sourceFiles , disabledTsetseRules ) . diagnostics ; } ) ; if ( diagnostics . length > 0 ) { console . error ( bazelDiagnostics . format ( bazelOpts . target , diagnostics ) ) ; return false ; } const perfTracePath = bazelOpts . perfTracePath ; if ( perfTracePath ) { log ( 'Writing trace to' , perfTracePath ) ; perfTrace . snapshotMemoryUsage ( ) ; perfTrace . write ( perfTracePath ) ; } return true ; } const expectDiagnosticsWhitelist = [ ] ; /** errorDiag produces an error diagnostic not bound to a file or location. */ function errorDiag ( messageText ) { return { category : ts . DiagnosticCategory . Error , code : 0 , file : undefined , start : undefined , length : undefined , messageText } ; } /**
 * createProgramAndEmit creates a ts.Program from the given options and emits it
 * according to them (e.g. including running various plugins and tsickle). It
 * returns the program and any diagnostics generated.
 *
 * Callers should check and emit diagnostics.
 */ export function createProgramAndEmit ( fileLoader , options , bazelOpts , files , disabledTsetseRules ) { perfTrace . snapshotMemoryUsage ( ) ; cache . resetStats ( ) ; cache . traceStats ( ) ; const compilerHostDelegate = ts . createCompilerHost ( { target : ts . ScriptTarget . ES5 } ) ; const moduleResolver = bazelOpts . isJsTranspilation ? makeJsModuleResolver ( bazelOpts . workspaceName ) : ts . resolveModuleName ; const assets = [ ] ; if ( bazelOpts . angularCompilerOptions ) { if ( bazelOpts . angularCompilerOptions . assets ) { assets . push ( ... bazelOpts . angularCompilerOptions . assets ) ; } } const tsickleCompilerHost = new CompilerHost ( [ ... files , ... assets ] , options , bazelOpts , compilerHostDelegate , fileLoader , moduleResolver ) ; let compilerHost = tsickleCompilerHost ; const diagnosticPlugins = [ ] ; let angularPlugin ; if ( bazelOpts . angularCompilerOptions ) { try { const ngOptions = bazelOpts . angularCompilerOptions ; ngOptions [ 'rootDir' ] = options . rootDir ; let angularPluginEntryPoint = '@angular/compiler-cli' ; const ngtsc = require ( angularPluginEntryPoint ) ; angularPlugin = new ngtsc . NgTscPlugin ( ngOptions ) ; diagnosticPlugins . push ( angularPlugin ! ) ; } catch ( e ) { return { diagnostics : [ errorDiag ( 'when using `ts_library(use_angular_plugin=True)`, ' + `you must install @angular/compiler-cli (was: ${ e })` ) ] } ; } compilerHost = angularPlugin ! . wrapHost ! ( compilerHost , files , options ) ; } const oldProgram = cache . getProgram ( bazelOpts . target ) ; const program = perfTrace . wrap ( 'createProgram' , ( ) => ts . createProgram ( compilerHost . inputFiles , options , compilerHost , oldProgram ) ) ; cache . putProgram ( bazelOpts . target , program ) ; let transformers = { before : [ ] , after : [ ] , afterDeclarations : [ ] } ; let ignoreForDiagnostics = new Set < ts . SourceFile > ( ) ; if ( angularPlugin ) { const angularSetup = angularPlugin . setupCompilation ( program ) ; ignoreForDiagnostics = angularSetup . ignoreForDiagnostics ; transformers = angularPlugin . createTransformers ( ) ; } for ( const pluginConfig of options [ 'plugins' ] as ts . PluginImport [ ] || [ ] ) { if ( pluginConfig . name === 'ts-lit-plugin' ) { const litTscPlugin = new ( require ( 'ts-lit-plugin/lib/bazel-plugin' ) . Plugin ) ( program , pluginConfig ) as DiagnosticPlugin ; diagnosticPlugins . push ( litTscPlugin ) ; } } if ( ! bazelOpts . isJsTranspilation ) { let diagnostics = gatherDiagnostics ( options , bazelOpts , program , disabledTsetseRules , diagnosticPlugins , ignoreForDiagnostics ) ; if ( ! expectDiagnosticsWhitelist . length || expectDiagnosticsWhitelist . some ( p => bazelOpts . target . startsWith ( p ) ) ) { diagnostics = bazelDiagnostics . filterExpected ( bazelOpts , diagnostics , bazelDiagnostics . uglyFormat ) ; } else if ( bazelOpts . expectedDiagnostics . length > 0 ) { diagnostics . push ( errorDiag ( `Only targets under ${ expectDiagnosticsWhitelist . join ( ', ' ) } can use ` + 'expected_diagnostics, but got ' + bazelOpts . target ) ) ; } if ( angularPlugin ) { cache . putProgram ( bazelOpts . target , angularPlugin . getNextProgram ! ( ) ) ; } if ( diagnostics . length > 0 ) { debug ( 'compilation failed at' , new Error ( ) . stack ! ) ; return { program , diagnostics } ; } } function isAngularFile ( sf ) { if ( ! /\.ng(factory|summary)\.ts$/ . test ( sf . fileName ) ) { return false ; } return isCompilationTarget ( bazelOpts , { fileName : sf . fileName . slice ( 0 , - 13 ) + '.ts' } as ts . SourceFile ) ; } const compilationTargets = program . getSourceFiles ( ) . filter ( sf => isCompilationTarget ( bazelOpts , sf ) || isAngularFile ( sf ) ) ; let diagnostics = [ ] ; let useTsickleEmit = bazelOpts . tsickle ; if ( useTsickleEmit ) { diagnostics = emitWithTsickle ( program , tsickleCompilerHost , compilationTargets , options , bazelOpts , transformers ) ; } else { diagnostics = emitWithTypescript ( program , compilationTargets , transformers ) ; } if ( diagnostics . length > 0 ) { debug ( 'compilation failed at' , new Error ( ) . stack ! ) ; } cache . printStats ( ) ; return { program , diagnostics } ; } function emitWithTypescript ( program , compilationTargets , transforms ) { const diagnostics = [ ] ; for ( const sf of compilationTargets ) { const result = program . emit ( sf , undefined , undefined , undefined , transforms ) ; diagnostics . push ( ... result . diagnostics ) ; } return diagnostics ; } /**
 * Runs the emit pipeline with Tsickle transformations - goog.module rewriting
 * and Closure types emitted included.
 * Exported to be used by the internal global refactoring tools.
 * TODO(radokirov): investigate using runWithOptions and making this private
 * again, if we can make compilerHosts match.
 */ export function emitWithTsickle ( program , compilerHost , compilationTargets , options , bazelOpts , transforms ) { const emitResults = [ ] ; const diagnostics = [ ] ; let optTsickle ; try { optTsickle = require ( 'tsickle' ) ; } catch ( e ) { if ( e . code !== 'MODULE_NOT_FOUND' ) { throw e ; } throw new Error ( 'When setting bazelOpts { tsickle: true }, ' + 'you must also add a devDependency on the tsickle npm package' ) ; } perfTrace . wrap ( 'emit' , ( ) => { for ( const sf of compilationTargets ) { perfTrace . wrap ( `emit ${ sf . fileName }` , ( ) => { emitResults . push ( optTsickle . emitWithTsickle ( program , compilerHost , compilerHost , options , sf , undefined , undefined , undefined , { beforeTs : transforms . before , afterTs : transforms . after , afterDeclarations : transforms . afterDeclarations } ) ) ; } ) ; } } ) ; const emitResult = optTsickle . mergeEmitResults ( emitResults ) ; diagnostics . push ( ... emitResult . diagnostics ) ; if ( diagnostics . length > 0 ) { return diagnostics ; } let externs = '/** @externs */\n' + '// generating externs was disabled using generate_externs=False\n' ; if ( bazelOpts . tsickleGenerateExterns ) { externs = optTsickle . getGeneratedExterns ( emitResult . externs , options . rootDir ! ) ; } if ( ! options . noEmit && bazelOpts . tsickleExternsPath ) { fs . writeFileSync ( bazelOpts . tsickleExternsPath , externs ) ; if ( bazelOpts . tsickleGenerateExterns && compilerHost . provideExternalModuleDtsNamespace ) { for ( const extern of compilationTargets ) { if ( ! extern . isDeclarationFile ) continue ; const outputBaseDir = options . outDir ! ; const relativeOutputPath = compilerHost . relativeOutputPath ( extern . fileName ) ; mkdirp ( outputBaseDir , path . dirname ( relativeOutputPath ) ) ; const outputPath = path . join ( outputBaseDir , relativeOutputPath ) ; const moduleName = compilerHost . pathToModuleName ( '' , extern . fileName ) ; fs . writeFileSync ( outputPath , `goog.module('${ moduleName }');\n` + `// Export an empty object of unknown type to allow imports.\n` + `// TODO: use typeof once available\n` + `exports = /** @type {?} */ ({});\n` ) ; } } } if ( ! options . noEmit && bazelOpts . manifest ) { perfTrace . wrap ( 'manifest' , ( ) => { const manifest = constructManifest ( emitResult . modulesManifest , compilerHost ) ; fs . writeFileSync ( bazelOpts . manifest , manifest ) ; } ) ; } return diagnostics ; } /**
 * Creates directories subdir (a slash separated relative path) starting from
 * base.
 */ function mkdirp ( base , subdir ) { const steps = subdir . split ( path . sep ) ; let current = base ; for ( let i = 0 ; i < steps . length ; i ++ ) { current = path . join ( current , steps [ i ] ) ; if ( ! fs . existsSync ( current ) ) fs . mkdirSync ( current ) ; } } /**
 * Resolve module filenames for JS modules.
 *
 * JS module resolution needs to be different because when transpiling JS we
 * do not pass in any dependencies, so the TS module resolver will not resolve
 * any files.
 *
 * Fortunately, JS module resolution is very simple. The imported module name
 * must either a relative path, or the workspace root (i.e. 'google3'),
 * so we can perform module resolution entirely based on file names, without
 * looking at the filesystem.
 */ function makeJsModuleResolver ( workspaceName ) { const workspaceModuleSpecifierPrefix = `${ workspaceName }/` ; const workspaceDir = `${ path . sep }${ workspaceName }${ path . sep }` ; function jsModuleResolver ( moduleName , containingFile , compilerOptions , host ) { let resolvedFileName ; if ( containingFile === '' ) { resolvedFileName = moduleName ; } else if ( moduleName . startsWith ( workspaceModuleSpecifierPrefix ) ) { const workspaceDirLocation = containingFile . indexOf ( workspaceDir ) ; if ( workspaceDirLocation < 0 ) { return { resolvedModule : undefined } ; } const absolutePathToWorkspaceDir = containingFile . slice ( 0 , workspaceDirLocation ) ; resolvedFileName = path . join ( absolutePathToWorkspaceDir , moduleName ) ; } else { if ( ! moduleName . startsWith ( './' ) && ! moduleName . startsWith ( '../' ) ) { throw new Error ( `Unsupported module import specifier: ${ JSON . stringify ( moduleName ) }.\n` + `JS module imports must either be relative paths ` + `(beginning with '.' or '..'), ` + `or they must begin with '${ workspaceName }/'.` ) ; } resolvedFileName = path . join ( path . dirname ( containingFile ) , moduleName ) ; } return { resolvedModule : { resolvedFileName , extension : ts . Extension . Js , packageId : undefined , isExternalLibraryImport : false } } ; } return jsModuleResolver ; } if ( require . main === module ) { process . exitCode = main ( process . argv . slice ( 2 ) ) ; }