import assert from "assert";
import {WatchSet, readAndWatchFile, sha1} from '../fs/watch';
import files, {
  symlinkWithOverwrite, realpath,
} from '../fs/files';
import NpmDiscards from './npm-discards';
import {Profile} from '../tool-env/profile';
import {
  optimisticReadFile,
  optimisticReaddir,
  optimisticStatOrNull,
  optimisticLStatOrNull,
  optimisticHashOrNull,
} from "../fs/optimistic";

// Builder is in charge of writing "bundles" to disk, which are
// directory trees such as site archives, programs, and packages.  In
// addition to writing data to files, it can copy or link in existing
// files and directories (keeping track of them in a WatchSet in order
// to trigger rebuilds appropriately).
//
// By default, Builder constructs the entire output directory from
// scratch under a temporary name, and then moves it into place.
// For efficient rebuilds, Builder can be given a `previousBuilder`,
// in which case it will write files into the existing output directory
// instead.
//
// On Windows (or when METEOR_DISABLE_BUILDER_IN_PLACE is set), Builder
// always creates a new output directory under a temporary name rather than
// using the old directory.  The reason is that we don't want rebuilding to
// interfere with the running app, and we rely on the fact that on OS X and
// Linux, if the process has opened a file for reading, it retains the file
// by its inode, not path, so it is safe to write a new file to the same path
// (or delete the file).
//
// Separate from that, Builder has a strategy of writing files under a temporary
// name and then renaming them.  This is to achieve an "atomic" write, meaning
// the server doesn't see a partially-written file that appears truncated.
//
// On Windows we copy files instead of symlinking them (see comments inline).

// Whether to support writing files into the same directory as a previous
// Builder on rebuild (rather than creating a new build directory and
// moving it into place).
const ENABLE_IN_PLACE_BUILDER_REPLACEMENT =
  (process.platform !== 'win32') &&
  ! process.env.METEOR_DISABLE_BUILDER_IN_PLACE;

// Options:
//  - outputPath: Required. Path to the directory that will hold the
//    bundle when building is complete. It should not exist (unless
//    previousBuilder is passed). Its parents will be created if necessary.
// - previousBuilder: Optional. An in-memory instance of Builder left
// from the previous iteration. It is assumed that the previous builder
// has completed its job successfully and its files are stored on the
// file system in the exact layout as described in its usedAsFile data
// structure; and the hashes of the contents correspond to the
// writtenHashes data strcture.
export default class Builder {
  constructor({
    outputPath,
    previousBuilder,
    // Even though in-place builds are disabled by default on some
    // platforms (Windows), they can be forcibly reenabled with this
    // option, in cases where it's safe and/or necessary to avoid
    // clobbering existing files.
    forceInPlaceBuild = false,
  }) {
    this.outputPath = outputPath;

    // Paths already written to. Map from canonicalized relPath (no
    // trailing slash) to true for a file, or false for a directory.
    this.usedAsFile = { '': false, '.': false };
    this.previousUsedAsFile = {};

    this.writtenHashes = {};
    this.createdSymlinks = {};
    this.previousWrittenHashes = {};
    this.previousCreatedSymlinks = {};

    // foo/bar => foo/.build1234.bar
    // Should we include a random number? The advantage is that multiple
    // builds can run in parallel. The disadvantage is that stale build
    // files hang around forever. For now, go with the former.
    const nonce = Math.floor(Math.random() * 999999);
    this.buildPath = files.pathJoin(files.pathDirname(this.outputPath),
                                    '.build' + nonce + "." +
                                    files.pathBasename(this.outputPath));

    let resetBuildPath = true;

    // If we have a previous builder and we are allowed to re-use it,
    // let's keep all the older files on the file-system and replace
    // only outdated ones + write the new files in the same path
    if (previousBuilder &&
        (forceInPlaceBuild || ENABLE_IN_PLACE_BUILDER_REPLACEMENT)) {
      if (previousBuilder.outputPath !== outputPath) {
        throw new Error(
          `previousBuilder option can only be set to a builder with the same output path.
Previous builder: ${previousBuilder.outputPath}, this builder: ${outputPath}`
        );
      }

      if (files.exists(previousBuilder.outputPath)) {
        // write files in-place in the output directory of the previous builder
        this.buildPath = previousBuilder.outputPath;

        this.previousWrittenHashes = previousBuilder.writtenHashes;
        this.previousUsedAsFile = previousBuilder.usedAsFile;
        this.previousCreatedSymlinks = previousBuilder.createdSymlinks;

        resetBuildPath = false;
      } else {
        resetBuildPath = true;
      }
    }

    // Build the output from scratch
    if (resetBuildPath) {
      files.rm_recursive(this.buildPath);
      files.mkdir_p(this.buildPath, 0o755);
    }

    this.watchSet = new WatchSet();

    // XXX cleaner error handling. don't make the humans read an
    // exception (and, make suitable for use in automated systems)
  }

  // Like mkdir_p, but records in self.usedAsFile that we have created
  // the directories, and takes a path relative to the bundle
  // root. Throws an exception on failure.
  _ensureDirectory(relPath) {
    const parts = files.pathNormalize(relPath).split(files.pathSep);
    if (parts.length > 1 && parts[parts.length - 1] === '') {
      // remove trailing slash
      parts.pop();
    }

    const partsSoFar = [];
    parts.forEach(part => {
      partsSoFar.push(part);
      const partial = partsSoFar.join(files.pathSep);
      if (! (partial in this.usedAsFile)) {
        let needToMkdir = true;
        if (partial in this.previousUsedAsFile) {
          if (this.previousUsedAsFile[partial]) {
            // was previously used as file, delete it, create a directory
            try {
              files.unlink(partial);
            } catch (e) {
              // If files.unlink(partial) failed because the file does not
              // exist, then we can just pretend the unlink succeeded.
              if (e.code !== "ENOENT") {
                throw e;
              }
            }
          } else {
            // is already a directory
            needToMkdir = false;
          }
        }

        if (needToMkdir) {
          // It's new -- create it
          files.mkdir_p(files.pathJoin(this.buildPath, partial), 0o755);
        }
        this.usedAsFile[partial] = false;
      } else if (this.usedAsFile[partial]) {
        // Already exists and is a file. Oops.
        throw new Error(`tried to make ${relPath} a directory but ${partial} is already a file`);
      } else {
        // Already exists and is a directory
      }
    });
  }

  // isDirectory defaults to false
  _sanitize(relPath, isDirectory) {
    const parts = relPath.split(files.pathSep);
    const partsOut = [];
    for (let i = 0; i < parts.length; i++) {
      let part = parts[i];
      const shouldBeFile = (i === parts.length - 1) && ! isDirectory;
      const mustBeUnique = (i === parts.length - 1);

      // Basic sanitization
      if (part.match(/^\.+$/)) {
        throw new Error(`Path contains forbidden segment '${part}'`);
      }

      part = part.replace(/[^a-zA-Z0-9._\:\-@#]/g, '_');

      // If at last component, pull extension (if any) off of part
      let ext = '';
      if (shouldBeFile) {
        const split = part.split('.');
        if (split.length > 1) {
          ext = "." + split.pop();
        }
        part = split.join('.');
      }

      // Make sure it's sufficiently unique
      let suffix = '';
      while (true) {
        const candidate = files.pathJoin(partsOut.join(files.pathSep), part + suffix + ext);
        if (candidate.length) {
          // If we've never heard of this, then it's unique enough.
          if (!(candidate in this.usedAsFile)) {
            break;
          }
          // If we want this bit to be a directory, and we don't need it to be
          // unique (ie, it isn't the very last bit), and it's currently a
          // directory, then that's OK.
          if (!(mustBeUnique || this.usedAsFile[candidate])) {
            break;
          }
          // OK, either we want it to be unique and it already exists; or it is
          // currently a file (and we want it to be either a different file or a
          // directory).  Try a new suffix.
        }

        suffix++; // first increment will do '' -> 1
      }

      partsOut.push(part + suffix + ext);
    }

    return partsOut.join(files.pathSep);
  }

  // Checks if a file with the same path and hash was written by
  // the previous builder. If it was, it adds it to the cache and makes
  // sure the parent directories exist and are part of the cache.
  //
  // Returns true if the file was already written
  usePreviousWrite(relPath, hash, sanitize) {
    relPath = this._normalizeFilePath(relPath, sanitize);

    if (this.previousWrittenHashes[relPath] === hash) {
      this._ensureDirectory(files.pathDirname(relPath));
      this.writtenHashes[relPath] = hash;
      this.usedAsFile[relPath] = true;
      return true;
    }

    return false;
  }

  _normalizeFilePath(relPath, sanitize) {
    // Ensure no trailing slash
    if (relPath.slice(-1) === files.pathSep) {
      relPath = relPath.slice(0, -1);
    }

    // In sanitize mode, ensure path does not contain segments like
    // '..', does not contain forbidden characters, and is unique.
    if (sanitize) {
      relPath = this._sanitize(relPath);
    }

    return relPath;
  }

  // Write either a buffer or the contents of a file to `relPath` (a
  // path to a file relative to the bundle root), creating it (and any
  // enclosing directories) if it doesn't exist yet. Exactly one of
  // `data` and or `file` must be passed.
  //
  // Options:
  // - data: a Buffer to write to relPath. Overrides `file`.
  // - file: a filename to write to relPath, as a string.
  // - sanitize: if true, then all components of the path are stripped
  //   of any potentially troubling characters, an exception is thrown
  //   if any path segments consist entirely of dots (eg, '..'), and
  //   if there is a file in the bundle with the same relPath, then
  //   the path is changed by adding a numeric suffix.
  // - hash: a sha1 string used to determine if the contents of the
  //   new file written is not cached.
  // - executable: if true, mark the file as executable.
  // - symlink: if set to a string, create a symlink to its value
  //
  // Returns the final canonicalize relPath that was written to.
  //
  // If `file` is used then it will be added to the builder's WatchSet.
  write(relPath, {data, file, hash, sanitize, executable, symlink}) {
    relPath = this._normalizeFilePath(relPath, sanitize);

    let getData = null;
    if (data) {
      if (! (data instanceof Buffer)) {
        throw new Error("data must be a Buffer");
      }
      if (file) {
        throw new Error("May only pass one of data and file, not both");
      }
      getData = () => data;
    } else if (file) {
      // postpone reading the file into memory
      getData = () => readAndWatchFile(this.watchSet, files.pathResolve(file));
    } else if (! symlink) {
      throw new Error('Builder can not write without either data or a file path or a symlink path: ' + relPath);
    }

    this._ensureDirectory(files.pathDirname(relPath));
    const absPath = files.pathJoin(this.buildPath, relPath);

    if (symlink) {
      symlinkWithOverwrite(symlink, absPath);
    } else {
      hash = hash || sha1(getData());

      // Write is called multiple times for assets when they have multiple urls for the same file
      if (this.previousWrittenHashes[relPath] !== hash && this.writtenHashes[relPath] !== hash) {

        // Builder is used to create build products, which should be read-only;
        // users shouldn't be manually editing automatically generated files and
        // expecting the results to "stick".
        const mode = executable ? 0o555 : 0o444

        if (this.buildPath === this.outputPath || this.writtenHashes[relPath]) {
          // atomicallyRewriteFile handles overwriting files that have already been created
          atomicallyRewriteFile(absPath, getData(), {
              mode
          });
        } else {
          // Since builder is not updating in place, and
          // this build is only used if every file is successfully written,
          // it is not important to write atomically.
          files.writeFile(absPath, getData(), {
            mode
          })
      }
      }

      this.writtenHashes[relPath] = hash;
    }
    this.usedAsFile[relPath] = true;

    return relPath;
  }

  copyTranspiledModules(relativePaths, {
    sourceRootDir,
    targetRootDir = this.outputPath,
    needToTranspile = files.inCheckout(),
  }) {
    if (!needToTranspile) {
      // If these files have already been transpiled, copy the transpiled files
      // (both .js and .js.map) directly to the builder output directory, without
      // recompiling them.
      relativePaths.forEach(relPath => {
        const jsPath = jsToTs(relPath);
        [jsPath, jsPath + ".map"].forEach(path => {
          this.write(path, {
            file: files.pathJoin(sourceRootDir, path),
          });
        });
      });
      return;
    }

    const babel = require("meteor-babel");
    const commonBabelOptions = babel.getDefaultOptions({
      nodeMajorVersion: parseInt(process.versions.node),
      typescript: true
    });
    commonBabelOptions.sourceMaps = true;

    const toolsDir = files.getCurrentToolsDir();
    const babelCacheDirectory =
      files.pathJoin(files.pathDirname(toolsDir), ".babel-cache");

    relativePaths.forEach(relPath => {
      assert.ok(!files.pathIsAbsolute(relPath), relPath);
      const fullPath = files.pathJoin(sourceRootDir, relPath);
      let inputFileContents = files.readFile(fullPath, "utf-8");

      // If certain behavior should be disabled in the transpiled code, the
      // #RemoveInProd comment can be added to strip out appropriate lines.
      inputFileContents = inputFileContents.replace(/^.*#RemoveInProd.*$/mg, "");

      var transpiled = babel.compile(inputFileContents, {
        ...commonBabelOptions,
        filename: relPath,
        sourceFileName: "/" + relPath,
      }, {
        cacheDirectory: babelCacheDirectory,
      });

      // The published implementation of the meteor-tool package should
      // contain only .js files, like any compiled TypeScript project.
      // This design has the unfortunate consequence of forbidding
      // explicit .ts file extensions in imported module identifier
      // strings, but that's just how it goes with TypeScript.
      let outputPath = jsToTs(relPath);

      const sourceMapUrlComment =
        "//# sourceMappingURL=" + files.pathBasename(outputPath + ".map");

      this.write(outputPath, {
        data: Buffer.from(transpiled.code + "\n" + sourceMapUrlComment, 'utf8')
      });

      // The babelOptions.sourceMapTarget option was deprecated in Babel
      // 7.0.0-beta.41: https://github.com/babel/babel/pull/7500
      const sourceMapTarget = outputPath + ".map";
      transpiled.map.file = sourceMapTarget;

      this.write(sourceMapTarget, {
        data: Buffer.from(JSON.stringify(transpiled.map), 'utf8')
      });
    });
  }

  // Serialize `data` as JSON and write it to `relPath` (a path to a
  // file relative to the bundle root), creating parent directories as
  // necessary. Throw an exception if the file already exists.
  writeJson(relPath, data) {
    // Ensure no trailing slash
    if (relPath.slice(-1) === files.pathSep) {
      relPath = relPath.slice(0, -1);
    }

    this._ensureDirectory(files.pathDirname(relPath));
    const absPath = files.pathJoin(this.buildPath, relPath);

    atomicallyRewriteFile(
      absPath,
      Buffer.from(JSON.stringify(data, null, 2), 'utf8'),
      {mode: 0o444});

    this.usedAsFile[relPath] = true;
  }

  // Add relPath to the list of "already taken" paths in the
  // bundle. This will cause write, when in sanitize mode, to never
  // pick this filename (and will prevent files that from being
  // written that would conflict with paths that we are expecting to
  // be directories). Calling this twice on the same relPath will
  // given an exception.
  //
  // Returns the *current* (temporary!) path to where the file or directory
  // lives. This is so you could use non-builder code to write into a reserved
  // directory.
  //
  // options:
  // - directory: set to true to reserve this relPath to be a
  //   directory rather than a file.
  reserve(relPath, {directory} = {}) {
    // Ensure no trailing slash
    if (relPath.slice(-1) === files.pathSep) {
      relPath = relPath.slice(0, -1);
    }

    const parts = relPath.split(files.pathSep);
    const partsSoFar = [];
    for (let i = 0; i < parts.length; i ++) {
      const part = parts[i];
      partsSoFar.push(part);
      const soFar = partsSoFar.join(files.pathSep);
      if (this.usedAsFile[soFar]) {
        throw new Error("Path reservation conflict: " + relPath);
      }

      const shouldBeDirectory = (i < parts.length - 1) || directory;
      if (shouldBeDirectory) {
        if (! (soFar in this.usedAsFile)) {
          let needToMkdir = true;
          if (soFar in this.previousUsedAsFile) {
            if (this.previousUsedAsFile[soFar]) {
              files.unlink(soFar);
            } else {
              needToMkdir = false;
            }
          }
          if (needToMkdir) {
            files.mkdir_p(files.pathJoin(this.buildPath, soFar), 0o755);
          }
          this.usedAsFile[soFar] = false;
        }
      } else {
        this.usedAsFile[soFar] = true;
      }
    }

    // Return the path we reserved.
    return files.pathJoin(this.buildPath, relPath);
  }

  // Generate and reserve a unique name for a file based on `relPath`,
  // and return it. If `relPath` is available (there is no file with
  // that name currently existing or reserved, it doesn't contain
  // forbidden characters, a prefix of it is not already in use as a
  // file rather than a directory) then the return value will be
  // `relPath`. Otherwise relPath will be modified to get the return
  // value, say by adding a numeric suffix to some path components
  // (preserving the file extension however) and deleting forbidden
  // characters. Throws an exception if relPath contains any segments
  // that are all dots (eg, '..').
  //
  // options:
  //
  // - directory: generate (and reserve) a name for a directory,
  //   rather than a file.
  generateFilename(relPath, {directory} = {}) {
    relPath = this._sanitize(relPath, directory);
    this.reserve(relPath, { directory });
    return relPath;
  }

  // Convenience wrapper around generateFilename and write.
  //
  // (Note that in the object returned by builder.enter, this method
  // is patched through directly rather than rewriting its inputs and
  // outputs. This is only valid because it does nothing with its inputs
  // and outputs other than send pass them to other methods.)
  writeToGeneratedFilename(relPath, writeOptions) {
    const generated = this.generateFilename(relPath);
    this.write(generated, writeOptions);
    return generated;
  }

  // A version of copyDirectory that works better for copying node_modules
  // directories when symlinks are involved.
  copyNodeModulesDirectory(options) {
    // Although the options.from directory should probably be a
    // node_modules directory, the only essential precondition here is
    // that the destination directory is a node_modules directory.
    // assert.strictEqual(files.pathBasename(options.from), "node_modules");
    assert.strictEqual(files.pathBasename(options.to), "node_modules");

    if (options.symlink) {
      // If we're going to use symlinks to speed up this copy, then we
      // need to make sure we've reserved all directories that are not
      // package directories, such as the node_modules directory itself,
      // as well as node_modules/meteor and the parent directories of any
      // scoped npm packages.
      this._ensureAllNonPackageDirectories(
        realpath(options.from),
        options.to
      );
    }

    // Call this._copyDirectory rather than this.copyDirectory so that the
    // subBuilder hacks from Builder#enter won't apply a second time.
    return this._copyDirectory(options);
  }

  _ensureAllNonPackageDirectories(absFromDir, relToDir) {
    const dirStat = optimisticStatOrNull(absFromDir);
    if (! (dirStat && dirStat.isDirectory())) {
      return;
    }

    const absFromPackageJson =
      files.pathJoin(absFromDir, "package.json");

    const stat = optimisticStatOrNull(absFromPackageJson);
    if (stat && stat.isFile()) {
      // If the directory has a package.json file, it's a package
      // directory, and we should not call this._ensureDirectory, so that
      // the package directory can later be symlinked in copyDirectory.
      return;
    }

    this._ensureDirectory(relToDir);

    optimisticReaddir(absFromDir).forEach(item => {
      this._ensureAllNonPackageDirectories(
        files.pathJoin(absFromDir, item),
        files.pathJoin(relToDir, item)
      );
    });
  }

  // Recursively copy a directory and all of its contents into the
  // bundle. But if the symlink option was passed to the Builder
  // constructor, then make a symlink instead, if possible.
  //
  // Unlike with files.cp_r, if a symlink is found, it is copied as a symlink.
  //
  // This does NOT add anything to the WatchSet.
  //
  // Options:
  // - from: source path on local disk to copy from
  // - to: relative path to a directory in the bundle that will
  //   receive the files
  // - ignore: array of regexps of filenames (that is, basenames) to
  //   ignore (they may still be visible in the output bundle if
  //   symlinks are being used).  Like with WatchSets, they match against
  //   entries that end with a slash if it's a directory.
  // - specificFiles: just copy these paths (specified as relative to 'to').
  // - symlink: true if the directory should be symlinked instead of copying
  copyDirectory(options) {
    // TODO(benjamn) Remove this wrapper when Builder#enter is no longer
    // implemented using ridiculous hacks.
    return this._copyDirectory(options);
  }

  _copyDirectory({
    from, to,
    ignore,
    specificFiles,
    symlink,
    npmDiscards,
    // Optional predicate to filter files and directories.
    filter,
  }) {
    if (to.slice(-1) === files.pathSep) {
      to = to.slice(0, -1);
    }

    if (symlink) {
      if (specificFiles) {
        throw new Error("can't copy only specific paths with a single symlink");
      }

      if (this.usedAsFile[to]) {
        throw new Error("tried to copy a directory onto " + to +
                        " but it is is already a file");
      }
    }

    ignore = ignore || [];
    let specificPaths = null;
    if (specificFiles) {
      specificPaths = {};
      specificFiles.forEach(f => {
        while (f !== '.') {
          specificPaths[files.pathJoin(to, f)] = true;
          f = files.pathDirname(f);
        }
      });
    }

    const rootDir = realpath(from);

    const walk = (absFrom, relTo) => {
      if (symlink && ! (relTo in this.usedAsFile)) {
        this._ensureDirectory(files.pathDirname(relTo));
        const absTo = files.pathResolve(this.buildPath, relTo);
        if (this.previousCreatedSymlinks[absFrom] !== relTo) {
          symlinkWithOverwrite(absFrom, absTo);
        }
        this.usedAsFile[relTo] = false;
        this.createdSymlinks[absFrom] = relTo;
        return;
      }

      this._ensureDirectory(relTo);

      optimisticReaddir(absFrom).forEach(item => {
        let thisAbsFrom = files.pathResolve(absFrom, item);
        const thisRelTo = files.pathJoin(relTo, item);

        if (specificPaths && !(thisRelTo in specificPaths)) {
          return;
        }

        // Returns files.realpath(thisAbsFrom), if it is external to
        // rootDir, using caching because this function might be called
        // more than once.
        let cachedExternalPath;
        const getExternalPath = () => {
          if (typeof cachedExternalPath !== "undefined") {
            return cachedExternalPath;
          }

          try {
            var real = realpath(thisAbsFrom);
          } catch (e) {
            if (e.code !== "ENOENT" &&
                e.code !== "ELOOP") {
              throw e;
            }
            return cachedExternalPath = false;
          }

          const isExternal =
            files.pathRelative(rootDir, real).startsWith("..");

          // Now cachedExternalPath is either a string or false.
          return cachedExternalPath = isExternal && real;
        };

        let fileStatus = optimisticLStatOrNull(thisAbsFrom);

        if (! symlink &&
            fileStatus &&
            fileStatus.isSymbolicLink()) {
          // If copyDirectory is not allowed to create symbolic links to
          // external files, and this file is a symbolic link that points
          // to an external file, update fileStatus so that we copy this
          // file as a normal file rather than as a symbolic link.
          const externalPath = getExternalPath();
          if (externalPath) {
            // Update fileStatus to match the actual file rather than the
            // symbolic link, thus forcing the file to be copied below.
            fileStatus = optimisticLStatOrNull(externalPath);
          }
        }

        if (! fileStatus) {
          // If the file did not exist, skip it.
          return;
        }

        let itemForMatch = item;
        const isDirectory = fileStatus.isDirectory();
        if (isDirectory) {
          itemForMatch += '/';
        }

        // skip excluded files
        if (ignore.some(pattern => itemForMatch.match(pattern))) {
          return;
        }

        if (typeof filter === "function" &&
            ! filter(thisAbsFrom, isDirectory)) {
          return;
        }

        if (npmDiscards instanceof NpmDiscards &&
            npmDiscards.shouldDiscard(thisAbsFrom, isDirectory)) {
          return;
        }

        if (isDirectory) {
          walk(thisAbsFrom, thisRelTo);
          return;
        }

        if (fileStatus.isSymbolicLink()) {
          // Symbolic links pointing to relative external paths are less
          // portable than absolute links, so getExternalPath() is
          // preferred if it returns a path.
          const linkSource = getExternalPath() ||
            files.readlink(thisAbsFrom);

          const linkTarget =
            files.pathResolve(this.buildPath, thisRelTo);

          if (symlinkIfPossible(linkSource, linkTarget)) {
            // A symlink counts as a file, as far as "can you put
            // something under it" goes.
            this.usedAsFile[thisRelTo] = true;
            return;
          }
        }

        // Fall back to copying the file, but make sure it's really a file
        // first, just in case it was a symbolic link to a directory that
        // could not be created above.
        fileStatus = optimisticStatOrNull(thisAbsFrom);
        if (fileStatus && fileStatus.isFile()) {
          const hash = optimisticHashOrNull(thisAbsFrom);

          if (this.previousWrittenHashes[thisRelTo] !== hash) {
            const content = optimisticReadFile(thisAbsFrom);

            files.writeFile(
              files.pathResolve(this.buildPath, thisRelTo),
              // The reason we call files.writeFile here instead of
              // files.copyFile is so that we can read the file using
              // optimisticReadFile instead of files.createReadStream.
              content,
              // Logic borrowed from files.copyFile: "Create the file as
              // readable and writable by everyone, and executable by everyone
              // if the original file is executably by owner. (This mode will be
              // modified by umask.) We don't copy the mode *directly* because
              // this function is used by 'meteor create' which is copying from
              // the read-only tools tree into a writable app."
              { mode: (fileStatus.mode & 0o100) ? 0o777 : 0o666 },
            );
          }

          this.writtenHashes[thisRelTo] = hash;
          this.usedAsFile[thisRelTo] = true;
        }
      });
    };

    walk(rootDir, to);
  }

  // Returns a new Builder-compatible object that works just like a
  // Builder, but interprets all paths relative to 'relPath', a path
  // relative to the bundle root which should not start with a '/'.
  //
  // The sub-builder returned does not have all Builder methods (for
  // example, complete() wouldn't make sense) and you should not rely
  // on it being instanceof Builder.
  //
  // TODO(benjamn) This nonsense should be ripped out by any means
  // necessary... whenever someone has the time.
  enter(relPath) {
    const subBuilder = {};
    const relPathWithSep = relPath + files.pathSep;
    const methods = [
      "write",
      "writeJson",
      "reserve",
      "generateFilename",
      "copyDirectory",
      "copyNodeModulesDirectory",
      "enter",
    ];

    methods.forEach(method => {
      subBuilder[method] = (...args) => {
        if (method === "copyDirectory" ||
            method === "copyNodeModulesDirectory") {
          // The copy methods take their relative paths via options.to.
          args[0].to = files.pathJoin(relPath, args[0].to);
        } else {
          // Other methods have relPath as the first argument.
          args[0] = files.pathJoin(relPath, args[0]);
        }

        let ret = this[method](...args);

        if (method === "generateFilename") {
          // fix up the returned path to be relative to the
          // sub-bundle, not the parent bundle
          if (ret.substr(0, 1) === '/') {
            ret = ret.substr(1);
          }
          if (ret.substr(0, relPathWithSep.length) !== relPathWithSep) {
            throw new Error("generateFilename returned path outside of " +
                            "sub-bundle?");
          }
          ret = ret.substr(relPathWithSep.length);
        }

        return ret;
      };
    });

    // Methods that don't have to fix up arguments or return values, because
    // they are implemented purely in terms of other methods which do.
    const passThroughMethods = [
      "writeToGeneratedFilename",
      "copyTranspiledModules",
    ];
    passThroughMethods.forEach(method => {
      subBuilder[method] = this[method];
    });

    return subBuilder;
  }

  // Move the completed bundle into its final location (outputPath)
  complete() {
    if (this.previousUsedAsFile) {
      // delete files and folders left-over from previous runs and not
      // re-used in this run
      const removed = {};
      const paths = Object.keys(this.previousUsedAsFile);
      paths.forEach((path) => {
        // if the same path was re-used, leave it
        if (this.usedAsFile.hasOwnProperty(path)) { return; }

        // otherwise, remove it as it is no longer needed

        // skip if already deleted
        if (removed.hasOwnProperty(path)) { return; }

        const absPath = files.pathJoin(this.buildPath, path);
        if (this.previousUsedAsFile[path]) {
          // file
          files.unlink(absPath);
          removed[path] = true;
        } else {
          // directory
          files.rm_recursive(absPath);

          // mark all sub-paths as removed, too
          paths.forEach((anotherPath) => {
            if (anotherPath.startsWith(path + '/')) {
              removed[anotherPath] = true;
            }
          });
        }
      });
    }

    // XXX Alternatively, we could just keep buildPath around, and make
    // outputPath be a symlink pointing to it. This doesn't work for the NPM use
    // case of renameDirAlmostAtomically since that one is constructing files to
    // be checked in to version control, but here we could get away with it.
    if (this.buildPath !== this.outputPath) {
      files.renameDirAlmostAtomically(this.buildPath, this.outputPath);
    }
  }

  // Delete the partially-completed bundle. Do not disturb outputPath.
  abort() {
    files.rm_recursive(this.buildPath);
  }

  // Returns a WatchSet representing all files that were read from disk by the
  // builder.
  getWatchSet() {
    return this.watchSet;
  }
}

function jsToTs(path) {
  if (path.endsWith(".ts")) {
    const parts = path.split(".");
    assert.strictEqual(parts.pop(), "ts");
    parts.push("js");
    path = parts.join(".");
  }
  return path;
}

function atomicallyRewriteFile(path, data, options) {
  // create a different file with a random name and then rename over atomically
  const rname = '.builder-tmp-file.' + Math.floor(Math.random() * 999999);
  const rpath = files.pathJoin(files.pathDirname(path), rname);
  files.writeFile(rpath, data, options);
  try {
    files.rename(rpath, path);
  } catch (e) {
    if (e.code === 'EISDIR') {
      // replacing a directory with a file; this is rare (so it can
      // be a slow path) but can legitimately happen if e.g. a developer
      // puts a file where there used to be a directory in their app.
      files.rm_recursive(path);
      files.rename(rpath, path);
    } else {
      throw e;
    }
  }
}

function symlinkIfPossible(source, target) {
  try {
    symlinkWithOverwrite(source, target);
    return true;
  } catch (e) {
    return false;
  }
}

// Wrap slow methods into Profiler calls
const slowBuilderMethods = [
  "_ensureDirectory",
  "write",
  "enter",
  "copyDirectory",
  "copyNodeModulesDirectory",
  "enter",
  "complete",
];

slowBuilderMethods.forEach(method => {
  Builder.prototype[method] =
    Profile(`Builder#${method}`, Builder.prototype[method]);
});


// Add slashes to the end of directories.

const contentsWithSlashes: string[] = [];

let stat: Dirent | Stats | null = entry;

// We do stat instead of lstat here, so that we treat symlinks to

keep keep keep replace keep keep keep keep

let stat: Dirent | Stats | BigIntStats | null = entry;


// Null until safeWatcher.watch succeeds in watching the file.

watcher: safeWatcher.SafeWatcher | null;

// Undefined until we stat the file for the first time, then null

// if the file is observed to be missing.

keep keep keep keep replace keep keep keep keep keep

import { FSWatcher, Stats } from "fs";
import { Profile } from "../tool-env/profile";
import {
  statOrNull,
  pathResolve,
  convertToOSPath,
  watchFile,
  unwatchFile,
} from "./files";

const watchLibrary = require("pathwatcher");

// Set METEOR_WATCH_FORCE_POLLING environment variable to a truthy value to
// force the use of files.watchFile instead of watchLibrary.watch.
var WATCHER_ENABLED = ! JSON.parse(
  process.env.METEOR_WATCH_FORCE_POLLING || "false"
);

// Default to prioritizing changed files, but disable that behavior (and
// thus prioritize all files equally) if METEOR_WATCH_PRIORITIZE_CHANGED
// is explicitly set to a string that parses to a falsy value.
var PRIORITIZE_CHANGED = true;
if (process.env.METEOR_WATCH_PRIORITIZE_CHANGED &&
    ! JSON.parse(process.env.METEOR_WATCH_PRIORITIZE_CHANGED)) {
  PRIORITIZE_CHANGED = false;
}

var DEFAULT_POLLING_INTERVAL =
  +(process.env.METEOR_WATCH_POLLING_INTERVAL_MS || 5000);

var NO_WATCHER_POLLING_INTERVAL =
  +(process.env.METEOR_WATCH_POLLING_INTERVAL_MS || 500);

// This may seems like a long time to wait before actually closing the
// file watchers, but it's to our advantage if they survive restarts.
const WATCHER_CLEANUP_DELAY_MS = 30000;

// Pathwatcher complains (using console.error, ugh) if you try to watch
// two files with the same stat.ino number but different paths on linux, so we have
// to deduplicate files by ino.
const DEDUPLICATE_BY_INO = process.platform !== "win32";

const entriesByIno = new Map;

export type SafeWatcher = {
  close: () => void;
}

type EntryCallback = (event: string) => void;

interface Entry extends SafeWatcher {
  callbacks: Set<EntryCallback>;
  rewatch: () => void;
  release: (callback: EntryCallback) => void;
}

const entries: Record<string, Entry | null> = Object.create(null);

// Set of paths for which a change event has been fired, watched with
// watchLibrary.watch if available. This could be an LRU cache, but in
// practice it should never grow large enough for that to matter.
const changedPaths = new Set;

function hasPriority(absPath: string) {
  // If we're not prioritizing changed files, then all files have
  // priority, which means they should be watched with native file
  // watchers if the platform supports them. If we are prioritizing
  // changed files, then only changed files get priority.
  return PRIORITIZE_CHANGED
    ? changedPaths.has(absPath)
    : true;
}

function acquireWatcher(absPath: string, callback: EntryCallback) {
  const entry = entries[absPath] || (
    entries[absPath] = startNewWatcher(absPath));

  // Watches successfully established in the past may have become invalid
  // because the watched file was deleted or renamed, so we need to make
  // sure we're still watching every time we call safeWatcher.watch.
  entry.rewatch();

  // The size of the entry.callbacks Set also serves as a reference count
  // for this watcher.
  entry.callbacks.add(callback);

  return entry;
}

function startNewWatcher(absPath: string): Entry {
  let stat: Stats | null = null;

  if (DEDUPLICATE_BY_INO) {
    stat = statOrNull(absPath);
    if (stat && stat.ino > 0 && entriesByIno.has(stat.ino)) {
      const entry = entriesByIno.get(stat.ino);
      if (entries[absPath] === entry) {
        return entry;
      }
    }
  } else {
    let entry = entries[absPath];
    if (entry) {
      return entry;
    }
  }

  function safeUnwatch() {
    if (watcher) {
      watcher.close();
      watcher = null;
      if (stat && stat.ino > 0) {
        entriesByIno.delete(stat.ino);
      }
    }
  }

  let lastWatcherEventTime = Date.now();
  const callbacks = new Set<EntryCallback>();
  let watcherCleanupTimer: ReturnType<typeof setTimeout> | null = null;
  let watcher: FSWatcher | null = null;

  // Determines the polling interval to be used for the fs.watchFile-based
  // safety net that works on all platforms and file systems.
  function getPollingInterval() {
    if (hasPriority(absPath)) {
      // Regardless of whether we have a native file watcher and it works
      // correctly on this file system, poll prioritized files (that is,
      // files that have been changed at least once) at a higher frequency
      // (every 500ms by default).
      return NO_WATCHER_POLLING_INTERVAL;
    }

    if (WATCHER_ENABLED || PRIORITIZE_CHANGED) {
      // As long as native file watching is enabled (even if it doesn't
      // work correctly) and the developer hasn't explicitly opted out of
      // the file watching priority system, poll unchanged files at a
      // lower frequency (every 5000ms by default).
      return DEFAULT_POLLING_INTERVAL;
    }

    // If native file watching is disabled and the developer has
    // explicitly opted out of the priority system, poll everything at the
    // higher frequency (every 500ms by default). Note that this leads to
    // higher idle CPU usage, so the developer may want to adjust the
    // METEOR_WATCH_POLLING_INTERVAL_MS environment variable.
    return NO_WATCHER_POLLING_INTERVAL;
  }

  function fire(event: string) {
    if (event !== "change") {
      // When we receive a "delete" or "rename" event, the watcher is
      // probably not going to generate any more notifications for this
      // file, so we close and nullify the watcher to ensure that
      // entry.rewatch() will attempt to reestablish the watcher the next
      // time we call safeWatcher.watch.
      safeUnwatch();

      // Make sure we don't throttle the watchFile callback after a
      // "delete" or "rename" event, since it is now our only reliable
      // source of file change notifications.
      lastWatcherEventTime = 0;

    } else {
      changedPaths.add(absPath);
      rewatch();
    }

    callbacks.forEach(cb => cb(event));
  }

  function watchWrapper(event: string) {
    lastWatcherEventTime = Date.now();
    fire(event);

    // It's tempting to call unwatchFile(absPath, watchFileWrapper) here,
    // but previous watcher success is no guarantee of future watcher
    // reliability. For example, watchLibrary.watch works just fine when file
    // changes originate from within a Vagrant VM, but changes to shared
    // files made outside the VM are invisible to watcher, so our only
    // hope of catching them is to continue polling.
  }

  function rewatch() {
    if (hasPriority(absPath)) {
      if (watcher) {
        // Already watching; nothing to do.
        return;
      }
      watcher = watchLibraryWatch(absPath, watchWrapper);
    } else if (watcher) {
      safeUnwatch();
    }

    // Since we're about to restart the stat-based file watcher, we don't
    // want to miss any of its events because of the lastWatcherEventTime
    // throttling that it attempts to do.
    lastWatcherEventTime = 0;

    // We use files.watchFile in addition to watcher.watch as a fail-safe
    // to detect file changes even on network file systems.  However
    // (unless the user disabled watcher or this watcher call failed), we
    // use a relatively long default polling interval of 5000ms to save
    // CPU cycles.
    statWatch(absPath, getPollingInterval(), watchFileWrapper);
  }

  function watchFileWrapper(newStat: Stats, oldStat: Stats) {
    if (newStat.ino === 0 &&
        oldStat.ino === 0 &&
        +newStat.mtime === +oldStat.mtime) {
      // Node calls the watchFile listener once with bogus identical stat
      // objects, which should not trigger a file change event.
      return;
    }

    // If a watcher event fired in the last polling interval, ignore
    // this event.
    if (Date.now() - lastWatcherEventTime > getPollingInterval()) {
      fire("change");
    }
  }

  const entry = {
    callbacks,
    rewatch,

    release(callback: EntryCallback) {
      if (! entries[absPath]) {
        return;
      }

      callbacks.delete(callback);
      if (callbacks.size > 0) {
        return;
      }

      // Once there are no more callbacks in the Set, close both watchers
      // and nullify the shared data.
      if (watcherCleanupTimer) {
        clearTimeout(watcherCleanupTimer);
      }

      watcherCleanupTimer = setTimeout(() => {
        if (callbacks.size > 0) {
          // If another callback was added while the timer was pending, we
          // can avoid tearing anything down.
          return;
        }
        entry.close();
      }, WATCHER_CLEANUP_DELAY_MS);
    },

    close() {
      if (entries[absPath] !== entry) return;
      entries[absPath] = null;

      if (watcherCleanupTimer) {
        clearTimeout(watcherCleanupTimer);
        watcherCleanupTimer = null;
      }

      safeUnwatch();

      unwatchFile(absPath, watchFileWrapper);
    }
  };

  if (stat && stat.ino > 0) {
    entriesByIno.set(stat.ino, entry);
  }

  return entry;
}

export function closeAllWatchers() {
  Object.keys(entries).forEach(absPath => {
    const entry = entries[absPath];
    if (entry) {
      entry.close();
    }
  });
}

const statWatchers = Object.create(null);

function statWatch(
  absPath: string,
  interval: number,
  callback: (current: Stats, previous: Stats) => void,
) {
  const oldWatcher = statWatchers[absPath];

  while (oldWatcher) {
    // Make sure this callback no longer appears among the listeners for
    // this StatWatcher.
    const countBefore = oldWatcher.stat.listenerCount("change");

    // This removes at most one occurrence of the callback from the
    // listeners list...
    oldWatcher.stat.removeListener("change", callback);

    // ... so we have to keep calling it until the first time
    // it removes nothing.
    if (oldWatcher.stat.listenerCount("change") === countBefore) {
      break;
    }
  }

  // This doesn't actually call newStat.start again if there's already a
  // watcher for this file, so it won't change any interval previously
  // specified. In the rare event that the interval needs to change, we
  // manually stop and restart the StatWatcher below.
  const newStat = watchFile(absPath, {
    persistent: false, // never persistent
    interval,
  }, callback);

  if (! oldWatcher) {
    const newWatcher = {
      stat: newStat,
      interval,
    };

    newStat.on("stop", () => {
      if (statWatchers[absPath] === newWatcher) {
        delete statWatchers[absPath];
      }
    });

    return statWatchers[absPath] = newWatcher;
  }

  // These should be identical at this point, but just in case.
  oldWatcher.stat = newStat;

  // If the interval needs to be changed, manually stop and restart the
  // StatWatcher using lower-level methods than unwatchFile and watchFile.
  if (oldWatcher.interval !== interval) {
    oldWatcher.stat.stop();
    oldWatcher.stat.start(
      convertToOSPath(pathResolve(absPath)),
      false, // never persistent
      oldWatcher.interval = interval,
    );
  }

  return oldWatcher;
}

function watchLibraryWatch(absPath: string, callback: EntryCallback) {
  if (WATCHER_ENABLED) {
    try {
      return watchLibrary.watch(convertToOSPath(absPath), callback);
    } catch (e) {
      maybeSuggestRaisingWatchLimit(e);
      // ... ignore the error.  We'll still have watchFile, which is good
      // enough.
    }
  }

  return null;
}

let suggestedRaisingWatchLimit = false;

// This function is async so that archinfo.host() (which may call
// utils.execFileSync) will run in a Fiber.
async function maybeSuggestRaisingWatchLimit(error: Error & { errno: number }) {
  var constants = require('constants');
  var archinfo = require('../utils/archinfo');
  if (! suggestedRaisingWatchLimit &&
      // Note: the not-super-documented require('constants') maps from
      // strings to SYSTEM errno values. System errno values aren't the same
      // as the numbers used internally by libuv! Once we're upgraded
      // to Node 0.12, we'll have the system errno as a string (on 'code'),
      // but the support for that wasn't in Node 0.10's uv.
      // See our PR https://github.com/atom/node-pathwatcher/pull/53
      // (and make sure to read the final commit message, not the original
      // proposed PR, which had a slightly different interface).
      error.errno === constants.ENOSPC &&
      // The only suggestion we currently have is for Linux.
      archinfo.matches(archinfo.host(), 'os.linux')) {

    // Check suggestedRaisingWatchLimit again because archinfo.host() may
    // have yielded.
    if (suggestedRaisingWatchLimit) return;
    suggestedRaisingWatchLimit = true;

    var Console = require('../console/console.js').Console;
    if (! Console.isHeadless()) {
      Console.arrowWarn(
        "It looks like a simple tweak to your system's configuration will " +
          "make many tools (including this Meteor command) more efficient. " +
          "To learn more, see " +
          Console.url("https://github.com/meteor/docs/blob/master/long-form/file-change-watcher-efficiency.md"));
    }
  }
}

export const watch = Profile(
  "safeWatcher.watch",
  (absPath: string, callback: EntryCallback) => {
    const entry = acquireWatcher(absPath, callback);
    return {
      close() {
        entry.release(callback);
      }
    } as SafeWatcher;
  }
);


@first-style: dotted;
@import "subdir/foo.import.less";

.el1 { border-style: @first-style; }
.el2 { border-style: @el2-style; }
.el3 { border-style: @el3-style; }

@import "{local-pack}/p.less";
.el4 { border-style: @el4-style; }