import * as fs from 'fs' ; import * as path from 'path' ; import * as tsickle from 'tsickle' ; import * as ts from 'typescript' ; import { FileLoader } from './cache' ; import * as perfTrace from './perf_trace' ; import { BazelOptions } from './tsconfig' ; import { DEBUG , debug } from './worker' ; export type ModuleResolver = ( moduleName : string , containingFile : string , compilerOptions : ts . CompilerOptions , host : ts . ModuleResolutionHost ) => ts . ResolvedModuleWithFailedLookupLocations ; /**
 * Narrows down the type of some properties from non-optional to required, so
 * that we do not need to check presence before each access.
 */ export interface BazelTsOptions extends ts . CompilerOptions { rootDirs : string [ ] ; rootDir : string ; outDir : string ; typeRoots : string [ ] ; } export function narrowTsOptions ( options ) { if ( ! options . rootDirs ) { throw new Error ( `compilerOptions.rootDirs should be set by tsconfig.bzl` ) ; } if ( ! options . rootDir ) { throw new Error ( `compilerOptions.rootDir should be set by tsconfig.bzl` ) ; } if ( ! options . outDir ) { throw new Error ( `compilerOptions.outDir should be set by tsconfig.bzl` ) ; } return options as BazelTsOptions ; } function validateBazelOptions ( bazelOpts ) { if ( ! bazelOpts . isJsTranspilation ) return ; if ( bazelOpts . compilationTargetSrc && bazelOpts . compilationTargetSrc . length > 1 ) { throw new Error ( 'In JS transpilation mode, only one file can appear in ' + 'bazelOptions.compilationTargetSrc.' ) ; } if ( ! bazelOpts . transpiledJsOutputFileName && ! bazelOpts . transpiledJsOutputDirectory ) { throw new Error ( 'In JS transpilation mode, either transpiledJsOutputFileName or ' + 'transpiledJsOutputDirectory must be specified in tsconfig.' ) ; } if ( bazelOpts . transpiledJsOutputFileName && bazelOpts . transpiledJsOutputDirectory ) { throw new Error ( 'In JS transpilation mode, cannot set both ' + 'transpiledJsOutputFileName and transpiledJsOutputDirectory.' ) ; } } const SOURCE_EXT = /((\.d)?\.tsx?|\.js)$/ ; /**
 * CompilerHost that knows how to cache parsed files to improve compile times.
 */ export class CompilerHost implements ts . CompilerHost , tsickle . TsickleHost { /**
     * Lookup table to answer file stat's without looking on disk.
     */ private knownFiles = new Set < string > ( ) ; /**
     * rootDirs relative to the rootDir, eg "bazel-out/local-fastbuild/bin"
     */ private relativeRoots : string [ ] ; getCancelationToken ? : ( ) => ts . CancellationToken ; directoryExists ? : ( dir : string ) => boolean ; googmodule : boolean ; es5Mode : boolean ; prelude : string ; untyped : boolean ; typeBlackListPaths : Set < string > ; transformDecorators : boolean ; transformTypesToClosure : boolean ; addDtsClutzAliases : boolean ; isJsTranspilation : boolean ; provideExternalModuleDtsNamespace : boolean ; options : BazelTsOptions ; moduleResolutionHost : ts . ModuleResolutionHost = this ; host : ts . ModuleResolutionHost = this ; private allowActionInputReads = true ; constructor ( public inputFiles , options , readonly bazelOpts , private delegate , private fileLoader , private moduleResolver = ts . resolveModuleName ) { this . options = narrowTsOptions ( options ) ; this . relativeRoots = this . options . rootDirs . map ( r => path . relative ( this . options . rootDir , r ) ) ; inputFiles . forEach ( ( f ) => { this . knownFiles . add ( f ) ; } ) ; if ( delegate && delegate . getCancellationToken ) { this . getCancelationToken = delegate . getCancellationToken . bind ( delegate ) ; } if ( this . allowActionInputReads && delegate && delegate . directoryExists ) { this . directoryExists = delegate . directoryExists . bind ( delegate ) ; } validateBazelOptions ( bazelOpts ) ; this . googmodule = bazelOpts . googmodule ; this . es5Mode = bazelOpts . es5Mode ; this . prelude = bazelOpts . prelude ; this . untyped = bazelOpts . untyped ; this . typeBlackListPaths = new Set ( bazelOpts . typeBlackListPaths ) ; this . transformDecorators = bazelOpts . tsickle ; this . transformTypesToClosure = bazelOpts . tsickle ; this . addDtsClutzAliases = bazelOpts . addDtsClutzAliases ; this . isJsTranspilation = Boolean ( bazelOpts . isJsTranspilation ) ; this . provideExternalModuleDtsNamespace = ! bazelOpts . hasImplementation ; } /**
     * For the given potentially absolute input file path (typically .ts), returns
     * the relative output path. For example, for
     * /path/to/root/blaze-out/k8-fastbuild/genfiles/my/file.ts, will return
     * my/file.js or my/file.mjs (depending on ES5 mode).
     */ relativeOutputPath ( fileName ) { let result = this . rootDirsRelative ( fileName ) ; result = result . replace ( /(\.d)?\.[jt]sx?$/ , '' ) ; if ( ! this . bazelOpts . es5Mode ) result += '.closure' ; return result + '.js' ; } /**
     * Workaround https://github.com/Microsoft/TypeScript/issues/8245
     * We use the `rootDirs` property both for module resolution,
     * and *also* to flatten the structure of the output directory
     * (as `rootDir` would do for a single root).
     * To do this, look for the pattern outDir/relativeRoots[i]/path/to/file
     * or relativeRoots[i]/path/to/file
     * and replace that with path/to/file
     */ flattenOutDir ( fileName ) { let result = fileName ; if ( fileName . startsWith ( this . options . rootDir ) ) { result = path . relative ( this . options . outDir , fileName ) ; } for ( const dir of this . relativeRoots ) { const rel = path . relative ( dir , result ) ; if ( ! rel . startsWith ( '..' ) ) { result = rel ; break ; } } return result ; } /** Avoid using tsickle on files that aren't in srcs[] */ shouldSkipTsickleProcessing ( fileName ) { return this . bazelOpts . isJsTranspilation || this . bazelOpts . compilationTargetSrc . indexOf ( fileName ) === - 1 ; } /** Whether the file is expected to be imported using a named module */ shouldNameModule ( fileName ) { return this . bazelOpts . compilationTargetSrc . indexOf ( fileName ) !== - 1 ; } /** Allows suppressing warnings for specific known libraries */ shouldIgnoreWarningsForPath ( filePath ) { return this . bazelOpts . ignoreWarningPaths . some ( p => ! ! filePath . match ( new RegExp ( p ) ) ) ; } param fileName return fileNameToModuleId ( fileName ) { return this . relativeOutputPath ( fileName . substring ( 0 , fileName . lastIndexOf ( '.' ) ) ) ; } return private rootDirsRelative ( fileName ) { for ( const root of this . options . rootDirs ) { if ( fileName . startsWith ( root ) ) { return path . posix . relative ( root , fileName ) ; } } return fileName ; } /**
     * Massages file names into valid goog.module names:
     * - resolves relative paths to the given context
     * - resolves non-relative paths which takes module_root into account
     * - replaces '/' with '.' in the '<workspace>' namespace
     * - replace first char if non-alpha
     * - replace subsequent non-alpha numeric chars
     */ pathToModuleName ( context , importPath ) { if ( context . startsWith ( this . options . outDir ) ) { context = path . join ( this . options . rootDir , path . relative ( this . options . outDir , context ) ) ; } let resolvedPath = null ; const resolved = this . moduleResolver ( importPath , context , this . options , this ) ; if ( resolved && resolved . resolvedModule && resolved . resolvedModule . resolvedFileName ) { resolvedPath = resolved . resolvedModule . resolvedFileName ; resolvedPath = this . rootDirsRelative ( resolvedPath ) ; } else { const trimmed = this . rootDirsRelative ( importPath ) ; if ( trimmed !== importPath ) { resolvedPath = trimmed ; } } if ( resolvedPath ) { importPath = resolvedPath . replace ( SOURCE_EXT , '' ) ; if ( importPath . indexOf ( this . bazelOpts . workspaceName ) !== 0 ) { importPath = path . posix . join ( this . bazelOpts . workspaceName , importPath ) ; } } if ( this . bazelOpts . locale ) { const suffix = '__' + this . bazelOpts . locale . toLowerCase ( ) ; if ( importPath . toLowerCase ( ) . endsWith ( suffix ) ) { importPath = importPath . substring ( 0 , importPath . length - suffix . length ) ; } } const escape = ( c ) => { return '$' + c . charCodeAt ( 0 ) . toString ( 16 ) ; } ; const moduleName = importPath . replace ( /^[0-9]|[^a-zA-Z_0-9_/]/g , escape ) . replace ( /\//g , '.' ) ; return moduleName ; } /**
     * Converts file path into a valid AMD module name.
     *
     * An AMD module can have an arbitrary name, so that it is require'd by name
     * rather than by path. See http://requirejs.org/docs/whyamd.html#namedmodules
     *
     * "However, tools that combine multiple modules together for performance need
     *  a way to give names to each module in the optimized file. For that, AMD
     *  allows a string as the first argument to define()"
     */ amdModuleName ( sf ) { if ( ! this . shouldNameModule ( sf . fileName ) ) return undefined ; let fileName = this . rootDirsRelative ( sf . fileName ) . replace ( SOURCE_EXT , '' ) ; let workspace = this . bazelOpts . workspaceName ; if ( fileName . startsWith ( 'external/' ) ) { const parts = fileName . split ( '/' ) ; workspace = parts [ 1 ] ; fileName = parts . slice ( 2 ) . join ( '/' ) ; } if ( this . bazelOpts . moduleName ) { const relativeFileName = path . posix . relative ( this . bazelOpts . package , fileName ) ; if ( ! relativeFileName . startsWith ( '..' ) ) { if ( this . bazelOpts . moduleRoot ) { const root = this . bazelOpts . moduleRoot . replace ( SOURCE_EXT , '' ) ; if ( root === relativeFileName || path . posix . join ( root , 'index' ) === relativeFileName ) { return this . bazelOpts . moduleName ; } } if ( relativeFileName === 'index' ) { return this . bazelOpts . moduleName ; } return path . posix . join ( this . bazelOpts . moduleName , relativeFileName ) ; } } if ( fileName . startsWith ( 'node_modules/' ) ) { return fileName . substring ( 'node_modules/' . length ) ; } return path . posix . join ( workspace , fileName ) ; } /**
     * Resolves the typings file from a package at the specified path. Helper
     * function to `resolveTypeReferenceDirectives`.
     */ private resolveTypingFromDirectory ( typePath , primary ) { const pkgFile = path . posix . join ( typePath , 'package.json' ) ; if ( this . fileExists ( pkgFile ) ) { const pkg = JSON . parse ( fs . readFileSync ( pkgFile , 'UTF-8' ) ) ; let typings = pkg [ 'typings' ] ; if ( typings ) { if ( typings === '.' || typings === './' ) { typings = 'index.d.ts' ; } const maybe = path . posix . join ( typePath , typings ) ; if ( this . fileExists ( maybe ) ) { return { primary , resolvedFileName : maybe } ; } } } const maybe = path . posix . join ( typePath , 'index.d.ts' ) ; if ( this . fileExists ( maybe ) ) { return { primary , resolvedFileName : maybe } ; } return undefined ; } /**
     * Override the default typescript resolveTypeReferenceDirectives function.
     * Resolves /// <reference types="x" /> directives under bazel. The default
     * typescript secondary search behavior needs to be overridden to support
     * looking under `bazelOpts.nodeModulesPrefix`
     */ resolveTypeReferenceDirectives ( names , containingFile ) { if ( ! this . allowActionInputReads ) return [ ] ; const result = [ ] ; names . forEach ( name => { let resolved ; this . options . typeRoots . forEach ( typeRoot => { if ( ! resolved ) { resolved = this . resolveTypingFromDirectory ( path . posix . join ( typeRoot , name ) , true ) ; } } ) ; if ( ! resolved ) { resolved = this . resolveTypingFromDirectory ( path . posix . join ( this . bazelOpts . nodeModulesPrefix , name ) , false ) ; } if ( ! resolved ) { if ( DEBUG ) { debug ( `Failed to resolve type reference directive '${ name }'` ) ; } return ; } result . push ( resolved as ts . ResolvedTypeReferenceDirective ) ; } ) ; return result ; } /** Loads a source file from disk (or the cache). */ getSourceFile ( fileName , languageVersion , onError ? ) { return perfTrace . wrap ( `getSourceFile ${ fileName }` , ( ) => { const sf = this . fileLoader . loadFile ( fileName , fileName , languageVersion ) as ts . SourceFile & { _hasGeneratedAmdModuleName ? : boolean ; } ; if ( ! /\.d\.tsx?$/ . test ( fileName ) && ( this . options . module === ts . ModuleKind . AMD || this . options . module === ts . ModuleKind . UMD ) ) { const moduleName = this . amdModuleName ( sf ) ; if ( sf . moduleName === moduleName || ! moduleName ) return sf ; if ( sf . moduleName ) { throw new Error ( `ERROR: ${ sf . fileName } ` + `contains a module name declaration ${ sf . moduleName } ` + `which would be overwritten with ${ moduleName } ` + `by Bazel's TypeScript compiler.` ) ; } sf . _hasGeneratedAmdModuleName = true ; sf . moduleName = moduleName ; return sf ; } if ( sf . _hasGeneratedAmdModuleName ) { sf . moduleName = undefined ; } return sf ; } ) ; } writeFile ( fileName , content , writeByteOrderMark , onError , sourceFiles ) { perfTrace . wrap ( `writeFile ${ fileName }` , ( ) => this . writeFileImpl ( fileName , content , writeByteOrderMark , onError , sourceFiles ) ) ; } writeFileImpl ( fileName , content , writeByteOrderMark , onError , sourceFiles ) { const version = ts . versionMajorMinor ; const [ major , minor ] = version . split ( '.' ) . map ( s => Number ( s ) ) ; const workaroundNeeded = major <= 2 && minor <= 8 ; if ( workaroundNeeded && ( this . options . module === ts . ModuleKind . AMD || this . options . module === ts . ModuleKind . UMD ) && fileName . endsWith ( '.d.ts' ) && sourceFiles && sourceFiles . length > 0 && sourceFiles [ 0 ] . moduleName ) { content = `/// <amd-module name="${ sourceFiles [ 0 ] . moduleName }" />\n${ content }` ; } fileName = this . flattenOutDir ( fileName ) ; if ( this . bazelOpts . isJsTranspilation ) { if ( this . bazelOpts . transpiledJsOutputFileName ) { fileName = this . bazelOpts . transpiledJsOutputFileName ! ; } else { fileName = path . relative ( this . bazelOpts . transpiledJsInputDirectory ! , fileName ) ; fileName = path . join ( this . bazelOpts . transpiledJsOutputDirectory ! , fileName ) ; } } else if ( ! this . bazelOpts . es5Mode ) { if ( this . bazelOpts . locale ) { fileName = fileName . replace ( /(__[^\.]+)?\.js$/ , '.closure$1.js' ) ; } else { fileName = fileName . replace ( /\.js$/ , '.mjs' ) ; } } fileName = path . join ( this . options . outDir , fileName ) ; if ( ! fs . existsSync ( fileName ) || fs . readFileSync ( fileName , 'utf-8' ) !== content ) { this . delegate . writeFile ( fileName , content , writeByteOrderMark , onError , sourceFiles ) ; } } /**
     * Performance optimization: don't try to stat files we weren't explicitly
     * given as inputs.
     * This also allows us to disable Bazel sandboxing, without accidentally
     * reading .ts inputs when .d.ts inputs are intended.
     * Note that in worker mode, the file cache will also guard against arbitrary
     * file reads.
     */ fileExists ( filePath ) { if ( this . allowActionInputReads && filePath . indexOf ( '/node_modules/' ) >= 0 ) { const result = this . fileLoader . fileExists ( filePath ) ; if ( DEBUG && ! result && this . delegate . fileExists ( filePath ) ) { debug ( "Path exists, but is not registered in the cache" , filePath ) ; Object . keys ( ( this . fileLoader as any ) . cache . lastDigests ) . forEach ( k => { if ( k . endsWith ( path . basename ( filePath ) ) ) { debug ( "  Maybe you meant to load from" , k ) ; } } ) ; } return result ; } return this . knownFiles . has ( filePath ) ; } getDefaultLibLocation ( ) { return path . dirname ( this . getDefaultLibFileName ( { target : ts . ScriptTarget . ES5 } ) ) ; } getDefaultLibFileName ( options ) { if ( this . bazelOpts . nodeModulesPrefix ) { return path . join ( this . bazelOpts . nodeModulesPrefix , 'typescript/lib' , ts . getDefaultLibFileName ( { target : ts . ScriptTarget . ES5 } ) ) ; } return this . delegate . getDefaultLibFileName ( options ) ; } realpath ( s ) { return s ; } getCanonicalFileName ( path ) { return this . delegate . getCanonicalFileName ( path ) ; } getCurrentDirectory ( ) { return this . delegate . getCurrentDirectory ( ) ; } useCaseSensitiveFileNames ( ) { return this . delegate . useCaseSensitiveFileNames ( ) ; } getNewLine ( ) { return this . delegate . getNewLine ( ) ; } getDirectories ( path ) { return this . delegate . getDirectories ? this . delegate . getDirectories ( path ) : [ ] ; } readFile ( fileName ) { return this . delegate . readFile ( fileName ) ; } trace ( s ) { console . error ( s ) ; } }