text: "use strict";
  Object.defineProperty(exports, "__esModule", { value: true });
  const fs = require("fs");
  const path = require("path");
  const ts = require("typescript");
  const perfTrace = require("./perf_trace");
  const worker_1 = require("./worker");
  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;
  }
  exports.narrowTsOptions = narrowTsOptions;
  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.
   */
  class CompilerHost {
      constructor(inputFiles, options, bazelOpts, delegate, fileLoader, moduleResolver = ts.resolveModuleName) {
          this.inputFiles = inputFiles;
          this.bazelOpts = bazelOpts;
          this.delegate = delegate;
          this.fileLoader = fileLoader;
          this.moduleResolver = moduleResolver;
          /**
           * Lookup table to answer file stat's without looking on disk.
           */
          this.knownFiles = new Set();
          this.moduleResolutionHost = this;
          // TODO(evanm): delete this once tsickle is updated.
          this.host = this;
          this.allowActionInputReads = true;
          this.options = narrowTsOptions(options);
          this.relativeRoots =
              this.options.rootDirs.map(r => path.relative(this.options.rootDir, r));
          inputFiles.forEach((f) => {
              this.knownFiles.add(f);
          });
          // getCancelationToken is an optional method on the delegate. If we
          // unconditionally implement the method, we will be forced to return null,
          // in the absense of the delegate method. That won't match the return type.
          // Instead, we optionally set a function to a field with the same name.
          if (delegate && delegate.getCancellationToken) {
              this.getCancelationToken = delegate.getCancellationToken.bind(delegate);
          }
          // Override directoryExists so that TypeScript can automatically
          // include global typings from node_modules/@types
          // see getAutomaticTypeDirectiveNames in
          // TypeScript:src/compiler/moduleNameResolver
          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;
          // outDir/relativeRoots[i]/path/to/file -> relativeRoots[i]/path/to/file
          if (fileName.startsWith(this.options.rootDir)) {
              result = path.relative(this.options.outDir, fileName);
          }
          for (const dir of this.relativeRoots) {
              // relativeRoots[i]/path/to/file -> path/to/file
              const rel = path.relative(dir, result);
              if (!rel.startsWith('..')) {
                  result = rel;
                  // relativeRoots is sorted longest first so we can short-circuit
                  // after the first match
                  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)));
      }
      /**
       * fileNameToModuleId gives the module ID for an input source file name.
       * @param fileName an input source file name, e.g.
       *     /root/dir/bazel-out/host/bin/my/file.ts.
       * @return the canonical path of a file within blaze, without /genfiles/ or
       *     /bin/ path parts, excluding a file extension. For example, "my/file".
       */
      fileNameToModuleId(fileName) {
          return this.relativeOutputPath(fileName.substring(0, fileName.lastIndexOf('.')));
      }
      /**
       * TypeScript SourceFile's have a path with the rootDirs[i] still present, eg.
       * /build/work/bazel-out/local-fastbuild/bin/path/to/file
       * @return the path without any rootDirs, eg. path/to/file
       */
      rootDirsRelative(fileName) {
          for (const root of this.options.rootDirs) {
              if (fileName.startsWith(root)) {
                  // rootDirs are sorted longest-first, so short-circuit the iteration
                  // see tsconfig.ts.
                  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) {
          // tsickle hands us an output path, we need to map it back to a source
          // path in order to do module resolution with it.
          // outDir/relativeRoots[i]/path/to/file ->
          // rootDir/relativeRoots[i]/path/to/file
          if (context.startsWith(this.options.outDir)) {
              context = path.join(this.options.rootDir, path.relative(this.options.outDir, context));
          }
          // Try to get the resolved path name from TS compiler host which can
          // handle resolution for libraries with module_root like rxjs and @angular.
          let resolvedPath = null;
          const resolved = this.moduleResolver(importPath, context, this.options, this);
          if (resolved && resolved.resolvedModule &&
              resolved.resolvedModule.resolvedFileName) {
              resolvedPath = resolved.resolvedModule.resolvedFileName;
              // /build/work/bazel-out/local-fastbuild/bin/path/to/file ->
              // path/to/file
              resolvedPath = this.rootDirsRelative(resolvedPath);
          }
          else {
              // importPath can be an absolute file path in google3.
              // Try to trim it as a path relative to bin and genfiles, and if so,
              // handle its file extension in the block below and prepend the workspace
              // name.
              const trimmed = this.rootDirsRelative(importPath);
              if (trimmed !== importPath) {
                  resolvedPath = trimmed;
              }
          }
          if (resolvedPath) {
              // Strip file extensions.
              importPath = resolvedPath.replace(SOURCE_EXT, '');
              // Make sure all module names include the workspace name.
              if (importPath.indexOf(this.bazelOpts.workspaceName) !== 0) {
                  importPath = path.posix.join(this.bazelOpts.workspaceName, importPath);
              }
          }
          // Remove the __{LOCALE} from the module name.
          if (this.bazelOpts.locale) {
              const suffix = '__' + this.bazelOpts.locale.toLowerCase();
              if (importPath.toLowerCase().endsWith(suffix)) {
                  importPath = importPath.substring(0, importPath.length - suffix.length);
              }
          }
          // Replace characters not supported by goog.module and '.' with
          // '$<Hex char code>' so that the original module name can be re-obtained
          // without any loss.
          // See goog.VALID_MODULE_RE_ in Closure's base.js for characters supported
          // by google.module.
          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;
          // /build/work/bazel-out/local-fastbuild/bin/path/to/file.ts
          // -> path/to/file
          let fileName = this.rootDirsRelative(sf.fileName).replace(SOURCE_EXT, '');
          let workspace = this.bazelOpts.workspaceName;
          // Workaround https://github.com/bazelbuild/bazel/issues/1262
          //
          // When the file comes from an external bazel repository,
          // and TypeScript resolves runfiles symlinks, then the path will look like
          // output_base/execroot/local_repo/external/another_repo/foo/bar
          // We want to name such a module "another_repo/foo/bar" just as it would be
          // named by code in that repository.
          // As a workaround, check for the /external/ path segment, and fix up the
          // workspace name to be the name of the external repository.
          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);
              // check that the fileName was actually underneath the package directory
              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;
                      }
                  }
                  // Support the common case of commonjs convention that index is the
                  // default module in a directory.
                  // This makes our module naming scheme more conventional and lets users
                  // refer to modules with the natural name they're used to.
                  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);
          }
          // path/to/file ->
          // myWorkspace/path/to/file
          return path.posix.join(workspace, fileName);
      }
      /**
       * Resolves the typings file from a package at the specified path. Helper
       * function to `resolveTypeReferenceDirectives`.
       */
      resolveTypingFromDirectory(typePath, primary) {
          // Looks for the `typings` attribute in a package.json file
          // if it exists
          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 };
                  }
              }
          }
          // Look for an index.d.ts file in the path
          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;
              // primary search
              this.options.typeRoots.forEach(typeRoot => {
                  if (!resolved) {
                      resolved = this.resolveTypingFromDirectory(path.posix.join(typeRoot, name), true);
                  }
              });
              // secondary search
              if (!resolved) {
                  resolved = this.resolveTypingFromDirectory(path.posix.join(this.bazelOpts.nodeModulesPrefix, name), false);
              }
              // Types not resolved should be silently ignored. Leave it to Typescript
              // to either error out with "TS2688: Cannot find type definition file for
              // 'foo'" or for the build to fail due to a missing type that is used.
              if (!resolved) {
                  if (worker_1.DEBUG) {
                      worker_1.debug(`Failed to resolve type reference directive '${name}'`);
                  }
                  return;
              }
              // In typescript 2.x the return type for this function
              // is `(ts.ResolvedTypeReferenceDirective | undefined)[]` thus we actually
              // do allow returning `undefined` in the array but the function is typed
              // `(ts.ResolvedTypeReferenceDirective)[]` to compile with both typescript
              // 2.x and 3.0/3.1 without error. Typescript 3.0/3.1 do handle the `undefined`
              // values in the array correctly despite the return signature.
              // It looks like the return type change was a mistake because
              // it was changed back to include `| undefined` recently:
              // https://github.com/Microsoft/TypeScript/pull/28059.
              result.push(resolved);
          });
          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);
              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.`);
                  }
                  // Setting the moduleName is equivalent to the original source having a
                  // ///<amd-module name="some/name"/> directive
                  sf.moduleName = moduleName;
              }
              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) {
          // Workaround https://github.com/Microsoft/TypeScript/issues/18648
          // This bug is fixed in TS 2.9
          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 {
                  // Strip the input directory path off of fileName to get the logical
                  // path within the input directory.
                  fileName =
                      path.relative(this.bazelOpts.transpiledJsInputDirectory, fileName);
                  // Then prepend the output directory name.
                  fileName =
                      path.join(this.bazelOpts.transpiledJsOutputDirectory, fileName);
              }
          }
          else if (!this.bazelOpts.es5Mode) {
              // Write ES6 transpiled files to *.mjs.
              if (this.bazelOpts.locale) {
                  // i18n paths are required to end with __locale.js so we put
                  // the .closure segment before the __locale
                  fileName = fileName.replace(/(__[^\.]+)?\.js$/, '.closure$1.js');
              }
              else {
                  fileName = fileName.replace(/\.js$/, '.mjs');
              }
          }
          // Prepend the output directory.
          fileName = path.join(this.options.outDir, fileName);
          // Our file cache is based on mtime - so avoid writing files if they
          // did not change.
          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) {
          // Under Bazel, users do not declare deps[] on their node_modules.
          // This means that we do not list all the needed .d.ts files in the files[]
          // section of tsconfig.json, and that is what populates the knownFiles set.
          // In addition, the node module resolver may need to read package.json files
          // and these are not permitted in the files[] section.
          // So we permit reading node_modules/* from action inputs, even though this
          // can include data[] dependencies and is broader than we would like.
          // This should only be enabled under Bazel, not Blaze.
          if (this.allowActionInputReads && filePath.indexOf('/node_modules/') >= 0) {
              const result = this.fileLoader.fileExists(filePath);
              if (worker_1.DEBUG && !result && this.delegate.fileExists(filePath)) {
                  worker_1.debug("Path exists, but is not registered in the cache", filePath);
                  Object.keys(this.fileLoader.cache.lastDigests).forEach(k => {
                      if (k.endsWith(path.basename(filePath))) {
                          worker_1.debug("  Maybe you meant to load from", k);
                      }
                  });
              }
              return result;
          }
          return this.knownFiles.has(filePath);
      }
      getDefaultLibLocation() {
          // Since we override getDefaultLibFileName below, we must also provide the
          // directory containing the file.
          // Otherwise TypeScript looks in C:\lib.xxx.d.ts for the default lib.
          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) {
          // tsc-wrapped relies on string matching of file paths for things like the
          // file cache and for strict deps checking.
          // TypeScript will try to resolve symlinks during module resolution which
          // makes our checks fail: the path we resolved as an input isn't the same
          // one the module resolver will look for.
          // See https://github.com/Microsoft/TypeScript/pull/12020
          // So we simply turn off symlink resolution.
          return s;
      }
      // Delegate everything else to the original compiler host.
      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);
      }
  }
  exports.CompilerHost = CompilerHost;
  