text: "use strict";
  Object.defineProperty(exports, "__esModule", { value: true });
  const Either_1 = require("fp-ts/lib/Either");
  const io_ts_reporters_1 = require("io-ts-reporters");
  const path_1 = require("path");
  const webpack_stats_1 = require("../interfaces/webpack-stats");
  const files_1 = require("../util/files");
  const strings_1 = require("../util/strings");
  // Note: Should only use with strings from `toPosixName()`.
  const NM_RE = /(^|\/)(node_modules|\~)(\/|$)/g;
  exports.nodeModulesParts = (name) => files_1.toPosixPath(name).split(NM_RE);
  // True if name is part of a `node_modules` path.
  exports._isNodeModules = (name) => exports.nodeModulesParts(name).length > 1;
  // Attempt to "unwind" webpack paths in `identifier` and `name` to remove
  // prefixes and produce a normal, usable filepath.
  //
  // First, strip off anything before a `?` and `!`:
  // - `REMOVE?KEEP`
  // - `REMOVE!KEEP`
  //
  // TODO(106): Revise code and tests for `fullPath`.
  // https://github.com/FormidableLabs/inspectpack/issues/106
  exports._normalizeWebpackPath = (identifier, name) => {
      const bangLastIdx = identifier.lastIndexOf("!");
      const questionLastIdx = identifier.lastIndexOf("?");
      const prefixEnd = Math.max(bangLastIdx, questionLastIdx);
      let candidate = identifier;
      // Remove prefix here.
      if (prefixEnd > -1) {
          candidate = candidate.substr(prefixEnd + 1);
      }
      // Naive heuristic: remove known starting webpack tokens.
      candidate = candidate.replace(/^(multi |ignored )/, "");
      // Assume a normalized then truncate to name if applicable.
      //
      // E.g.,
      // - `identifier`: "css /PATH/TO/node_modules/cache-loader/dist/cjs.js!STUFF
      //   !/PATH/TO/node_modules/font-awesome/css/font-awesome.css 0"
      // - `name`: "node_modules/font-awesome/css/font-awesome.css"
      //
      // Forms of name:
      // - v1, v2: "/PATH/TO/ROOT/~/pkg/index.js"
      // - v3: "/PATH/TO/ROOT/node_modules/pkg/index.js"
      // - v4: "./node_modules/pkg/index.js"
      if (name) {
          name = name
              .replace("/~/", "/node_modules/")
              .replace("\\~\\", "\\node_modules\\");
          if (name.startsWith("./") || name.startsWith(".\\")) {
              // Remove dot-slash relative part.
              name = name.slice(2);
          }
          // Now, truncate suffix of the candidate if name has less.
          const nameLastIdx = candidate.lastIndexOf(name);
          if (nameLastIdx > -1 && candidate.length !== nameLastIdx + name.length) {
              candidate = candidate.substr(0, nameLastIdx + name.length);
          }
      }
      return candidate;
  };
  // Convert a `node_modules` name to a base name.
  //
  // **Note**: Assumes only passed `node_modules` values.
  //
  // Normalizations:
  // - Remove starting path if `./`
  // - Switch Windows paths to Mac/Unix style.
  exports._getBaseName = (name) => {
      // Slice to just after last occurrence of node_modules.
      const parts = exports.nodeModulesParts(name);
      const lastName = parts[parts.length - 1];
      // Normalize out the rest of the string.
      let candidate = path_1.normalize(path_1.relative(".", lastName));
      // Short-circuit on empty string / current path.
      if (candidate === ".") {
          return "";
      }
      // Special case -- synthetic modules can end up with trailing `/` because
      // of a regular expression. Preserve this.
      //
      // E.g., `/PATH/TO/node_modules/moment/locale sync /es/`
      //
      // **Note**: The rest of this tranform _should_ be safe for synthetic regexps,
      // but we can always revisit.
      if (name[name.length - 1] === "/") {
          candidate += "/";
      }
      return files_1.toPosixPath(candidate);
  };
  class Action {
      constructor({ stats, ignoredPackages }) {
          this.stats = stats;
          this._ignoredPackages = (ignoredPackages || [])
              .map((pattern) => typeof pattern === "string" ? `${pattern}/` : pattern);
      }
      validate() {
          return Promise.resolve()
              .then(() => {
              // Validate the stats object.
              const result = webpack_stats_1.RWebpackStats.decode(this.stats);
              if (Either_1.isLeft(result)) {
                  const errs = io_ts_reporters_1.reporter(result);
                  throw new Error(`Invalid webpack stats object. (Errors: ${errs.join(", ")})`);
              }
          })
              .then(() => this);
      }
      // Create the internal data object for this action.
      //
      // This is a memoizing wrapper on the abstract internal method actions
      // must implement.
      getData() {
          return Promise.resolve()
              .then(() => this._data || this._getData())
              .then((data) => this._data = data);
      }
      // Flat array of webpack source modules only. (Memoized)
      get modules() {
          return this._modules = this._modules || this.getSourceMods(this.stats.modules);
      }
      ignorePackage(baseName) {
          const base = files_1.toPosixPath(baseName.trim());
          return this._ignoredPackages.some((pattern) => typeof pattern === "string" ?
              base.startsWith(pattern) :
              pattern.test(base));
      }
      getSourceMods(mods, parentChunks) {
          return mods
              // Recursively flatten to list of source modules.
              .reduce((list, mod) => {
              // Add in any parent chunks and ensure unique array.
              const chunks = Array.from(new Set(mod.chunks.concat(parentChunks || [])));
              // Fields
              let isSynthetic = false;
              let source = null;
              let identifier;
              let name;
              let size;
              if (Either_1.isRight(webpack_stats_1.RWebpackStatsModuleModules.decode(mod))) {
                  // Recursive case -- more modules.
                  const modsMod = mod;
                  // Return and recurse.
                  return list.concat(this.getSourceMods(modsMod.modules, chunks));
              }
              else if (Either_1.isRight(webpack_stats_1.RWebpackStatsModuleSource.decode(mod))) {
                  // Easy case -- a normal source code module.
                  const srcMod = mod;
                  identifier = srcMod.identifier;
                  name = srcMod.name;
                  // Note: there are isolated cases where webpack4 appears to be
                  // wrong in it's `size` estimation vs the actual string length.
                  // See `version mismatch for v1-v4 moment-app` wherein the
                  // real length of `moment/locale/es-us.js` is 3017 but webpack
                  // v4 reports it in stats object as 3029.
                  size = srcMod.source.length || srcMod.size;
                  source = srcMod.source;
              }
              else if (Either_1.isRight(webpack_stats_1.RWebpackStatsModuleSynthetic.decode(mod))) {
                  // Catch-all case -- a module without modules or source.
                  const syntheticMod = mod;
                  identifier = syntheticMod.identifier;
                  name = syntheticMod.name;
                  size = syntheticMod.size;
                  isSynthetic = true;
              }
              else {
                  throw new Error(`Cannot match to known module type: ${JSON.stringify(mod)}`);
              }
              // We've now got a single entry to prepare and add.
              const normalizedName = exports._normalizeWebpackPath(name);
              const normalizedId = exports._normalizeWebpackPath(identifier, normalizedName);
              const isNodeModules = exports._isNodeModules(normalizedId);
              const baseName = isNodeModules ? exports._getBaseName(normalizedId) : null;
              if (baseName && this.ignorePackage(baseName)) {
                  return list;
              }
              return list.concat([{
                      baseName,
                      chunks,
                      identifier,
                      isNodeModules,
                      isSynthetic,
                      size,
                      source,
                  }]);
          }, [])
              // Sort: via https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare
              .sort((a, b) => a.identifier.localeCompare(b.identifier));
      }
      // Object of source modules grouped by asset. (Memoized)
      get assets() {
          return this._assets = this._assets || this.getSourceAssets(this.stats.assets);
      }
      getSourceAssets(assets) {
          // Helper: LUT from chunk to asset name.
          const chunksToAssets = {};
          // Actual working data object.
          const modulesSetByAsset = {};
          // Limit assets to possible JS files.
          const jsAssets = assets.filter((asset) => /\.(m|)js$/.test(asset.name));
          // Iterate assets and begin populating structures.
          jsAssets.forEach((asset) => {
              modulesSetByAsset[asset.name] = {
                  asset,
                  mods: new Set(),
              };
              asset.chunks.forEach((chunk) => {
                  // Skip null chunks, allowing only strings or numbers.
                  if (chunk === null) {
                      return;
                  }
                  chunk = chunk.toString(); // force to string.
                  chunksToAssets[chunk] = chunksToAssets[chunk] || new Set();
                  // Add unique assets.
                  chunksToAssets[chunk].add(asset.name);
              });
          });
          // Iterate modules and attach as appropriate.
          this.modules.forEach((mod) => {
              mod.chunks.forEach((chunk) => {
                  // Skip null chunks, allowing only strings or numbers.
                  if (chunk === null) {
                      return;
                  }
                  chunk = chunk.toString(); // force to string.
                  (chunksToAssets[chunk] || []).forEach((assetName) => {
                      const assetObj = modulesSetByAsset[assetName];
                      if (assetObj) {
                          assetObj.mods.add(mod);
                      }
                  });
              });
          });
          // Convert to final form
          return Object.keys(modulesSetByAsset)
              .sort(strings_1.sort)
              .reduce((memo, assetName) => {
              const assetSetObj = modulesSetByAsset[assetName];
              memo[assetName] = {
                  asset: assetSetObj.asset,
                  mods: Array.from(assetSetObj.mods),
              };
              return memo;
          }, {});
      }
      get template() {
          this._template = this._template || this._createTemplate();
          return this._template;
      }
  }
  exports.Action = Action;
  var TemplateFormat;
  (function (TemplateFormat) {
      TemplateFormat["json"] = "json";
      TemplateFormat["text"] = "text";
      TemplateFormat["tsv"] = "tsv";
  })(TemplateFormat = exports.TemplateFormat || (exports.TemplateFormat = {}));
  class Template {
      constructor({ action }) {
          this.action = action;
      }
      json() {
          return this.action.getData().then((data) => JSON.stringify(data, null, 2));
      }
      render(format) {
          return this[format]();
      }
      trim(str, num) {
          return str
              .trimRight() // trailing space.
              .replace(/^[ ]*\s*/m, "") // First line, if empty.
              .replace(new RegExp(`^[ ]{${num}}`, "gm"), "");
      }
  }
  exports.Template = Template;
  