<commit_before>import {
  isString,
  isFunction,
  each,
  has,
} from "underscore";

import { sha1 } from "../fs/watch.js";
import { matches as archMatches } from "../utils/archinfo.js";
import {
  pathJoin,
  pathRelative,
  pathNormalize,
  pathDirname,
  convertToOSPath,
  convertToPosixPath,
} from "../fs/files.js";

import LRU from "lru-cache";

import { wrap } from "optimism";
import {
  optimisticStatOrNull,
  optimisticReadJsonOrNull,
} from "../fs/optimistic.js";

const nativeModulesMap = Object.create(null);
const nativeNames = Object.keys(process.binding("natives"));

// Node 0.10 does not include process as a built-in module, but later
// versions of Node do, and we provide a stub for it on the client.
nativeNames.push("process");

nativeNames.forEach(id => {
  if (id.startsWith("internal/")) {
    return;
  }

  // When a native Node module is imported, we register a dependency on a
  // meteor-node-stubs/deps/* module of the same name, so that the
  // necessary stub modules will be included in the bundle. This alternate
  // identifier will not be imported at runtime, but the modules it
  // depends on are necessary for the original import to succeed.
  nativeModulesMap[id] =  "meteor-node-stubs/deps/" + id;
});

const resolverCache = new LRU({
  max: Math.pow(2, 12)
});

export default class Resolver {
  static getOrCreate(options) {
    const key = JSON.stringify(options);
    let resolver = resolverCache.get(key);
    if (! resolver) {
      resolverCache.set(key, resolver = new Resolver(options));
    }
    return resolver;
  }

  constructor({
    sourceRoot,
    targetArch,
    extensions = [".js", ".json"],
    nodeModulesPaths = [],
  }) {
    this.sourceRoot = sourceRoot;
    this.extensions = extensions;
    this.targetArch = targetArch;
    this.nodeModulesPaths = nodeModulesPaths;
    this.statOrNull = optimisticStatOrNull;

    this.resolve = wrap((id, absParentPath) => {
      return this._resolve(id, absParentPath);
    }, {
      makeCacheKey(id, absParentPath) {
        // Only the directory of the absParentPath matters for caching.
        return JSON.stringify([id, pathDirname(absParentPath)]);
      }
    });
  }

  static isTopLevel(id) {
    return "./".indexOf(id.charAt(0)) < 0;
  }

  static isNative(id) {
    return has(nativeModulesMap, id);
  }

  static getNativeStubId(id) {
    return nativeModulesMap[id] || null;
  }

  // Resolve the given module identifier to an object { path, stat } or
  // null, relative to an absolute parent path. The _seenDirPaths
  // parameter is for internal use only and should be ommitted.
  _resolve(id, absParentPath, _seenDirPaths) {
    let resolved =
      this._resolveAbsolute(id, absParentPath) ||
      this._resolveRelative(id, absParentPath) ||
      this._resolveNodeModule(id, absParentPath);

    if (typeof resolved === "string") {
      // The _resolveNodeModule method can return "missing" to indicate
      // that the ImportScanner should look elsewhere for this module,
      // such as in the app node_modules directory.
      return resolved;
    }

    let packageJsonMap = null;

    while (resolved && resolved.stat.isDirectory()) {
      let dirPath = resolved.path;
      _seenDirPaths = _seenDirPaths || new Set;

      // If the "main" field of a package.json file resolves to a
      // directory we've already considered, then we should not attempt to
      // read the same package.json file again.
      if (! _seenDirPaths.has(dirPath)) {
        _seenDirPaths.add(dirPath);
        resolved = this._resolvePkgJsonMain(dirPath, _seenDirPaths);
        if (resolved) {
          // The _resolvePkgJsonMain call above may have returned a
          // directory, so first merge resolved.packageJsonMap into
          // packageJsonMap so that we don't forget the package.json we
          // just resolved, then continue the loop to make sure we fully
          // resolve the "main" module identifier to a non-directory.
          // Technically this could involve even more package.json files,
          // but in practice the "main" property will almost always name a
          // directory containing an index.js file.
          Object.assign(
            packageJsonMap || (packageJsonMap = Object.create(null)),
            resolved.packageJsonMap,
          );
          continue;
        }
      }

      // If we didn't find a `package.json` file, or it didn't have a
      // resolvable `.main` property, the only possibility left to
      // consider is that this directory contains an `index.js` module.
      // This assignment almost always terminates the while loop, because
      // there's very little chance an `index.js` file will be a
      // directory. However, in principle it is remotely possible that a
      // file called `index.js` could be a directory instead of a file.
      resolved = this._joinAndStat(dirPath, "index.js");
    }

    if (resolved) {
      if (packageJsonMap) {
        resolved.packageJsonMap = packageJsonMap;
      }

      resolved.id = convertToPosixPath(
        convertToOSPath(resolved.path),
        true
      );
    }

    return resolved;
  }

  _joinAndStat(...joinArgs) {
    const joined = pathJoin(...joinArgs);
    const path = pathNormalize(joined);
    const exactStat = this.statOrNull(path);
    const exactResult = exactStat && { path, stat: exactStat };
    let result = null;
    if (exactResult && exactStat.isFile()) {
      result = exactResult;
    }

    if (! result) {
      this.extensions.some(ext => {
        const pathWithExt = path + ext;
        const stat = this.statOrNull(pathWithExt);
        if (stat && ! stat.isDirectory()) {
          return result = { path: pathWithExt, stat };
        }
      });
    }

    if (! result && exactResult && exactStat.isDirectory()) {
      // After trying all available file extensions, fall back to the
      // original result if it was a directory.
      result = exactResult;
    }

    return result;
  }

  _resolveAbsolute(id, absParentPath) {
    return id.charAt(0) === "/" &&
      this._joinAndStat(this.sourceRoot, id.slice(1));
  }

  _resolveRelative(id, absParentPath) {
    if (id.charAt(0) === ".") {
      return this._joinAndStat(absParentPath, "..", id);
    }
  }

  _resolveNodeModule(id, absParentPath) {
    if (! Resolver.isTopLevel(id)) {
      return null;
    }

    if (Resolver.isNative(id) &&
        archMatches(this.targetArch, "os")) {
      // Forbid installing any server module with the same name as a
      // native Node module.
      return null;
    }

    let sourceRoot;
    const relParentPath = pathRelative(this.sourceRoot, absParentPath);
    if (! relParentPath.startsWith("..")) {
      // If the file is contained by this.sourceRoot, then it's safe to
      // use this.sourceRoot as the limiting ancestor directory in the
      // while loop below, but we're still going to check whether the file
      // resides in an external node_modules directory, since "external"
      // .npm/package/node_modules directories are technically contained
      // within the root directory of their packages.
      sourceRoot = this.sourceRoot;
    }

    this.nodeModulesPaths.some(path => {
      if (! pathRelative(path, absParentPath).startsWith("..")) {
        // If the file is inside an external node_modules directory,
        // consider the rootDir to be the parent directory of that
        // node_modules directory, rather than this.sourceRoot.
        return sourceRoot = pathDirname(path);
      }
    });

    let resolved = null;

    if (sourceRoot) {
      let dir = absParentPath; // It's ok for absParentPath to be a directory!
      let info = this._joinAndStat(dir);
      if (! info || ! info.stat.isDirectory()) {
        dir = pathDirname(dir);
      }

      while (! (resolved = this._joinAndStat(dir, "node_modules", id))) {
        if (dir === sourceRoot) {
          break;
        }

        const parentDir = pathDirname(dir);
        if (dir === parentDir) {
          // We've reached the root of the file system??
          break;
        }

        dir = parentDir;
      }
    }

    if (! resolved) {
      // After checking any local node_modules directories, fall back to
      // the package NPM directory, if one was specified.
      this.nodeModulesPaths.some(path => {
        return resolved = this._joinAndStat(path, id);
      });
    }

    // If the dependency is still not resolved, it might be handled by the
    // fallback function defined in meteor/packages/modules/modules.js, or
    // it might be imported in code that will never run on this platform,
    // so there is always the possibility that its absence is not actually
    // a problem. As much as we might like to issue warnings about missing
    // dependencies here, we just don't have enough information to make
    // that determination until the code actually runs.

    return resolved || "missing";
  }

  _resolvePkgJsonMain(dirPath, _seenDirPaths) {
    const pkgJsonPath = pathJoin(dirPath, "package.json");
    const pkg = optimisticReadJsonOrNull(pkgJsonPath);
    if (! pkg) {
      return null;
    }

    // Output a JS module that exports just the "name", "version", "main",
    // and "browser" properties (if defined) from the package.json file.
    const pkgSubset = {};

    if (has(pkg, "name")) {
      pkgSubset.name = pkg.name;
    }

    if (has(pkg, "version")) {
      pkgSubset.version = pkg.version;
    }

    let main;
    function tryMain(name) {
      const value = pkg[name];
      if (isString(value)) {
        main = main || value;
        pkgSubset[name] = value;
      }
    }

    if (archMatches(this.targetArch, "web")) {
      tryMain("browser");
    }

    tryMain("main");

    if (isString(main)) {
      // The "main" field of package.json does not have to begin with ./
      // to be considered relative, so first we try simply appending it to
      // the directory path before falling back to a full resolve, which
      // might return a package from a node_modules directory.
      const resolved = this._joinAndStat(dirPath, main) ||
        this._resolve(main, pkgJsonPath, _seenDirPaths);

      if (resolved && typeof resolved === "object") {
        if (! resolved.packageJsonMap) {
          resolved.packageJsonMap = Object.create(null);
        }

        resolved.packageJsonMap[pkgJsonPath] = pkgSubset;

        return resolved;
      }
    }

    return null;
  }
};

import { Profile } from "../tool-env/profile.js";
each(Resolver.prototype, (value, key) => {
  if (key === "constructor") return;
  Resolver.prototype[key] = Profile(
    `Resolver#${key}`,
    Resolver.prototype[key]
  );
});
<commit_msg>Refactor Resolver handling of package.json files.<commit_after>import {
  isString,
  isObject,
  isFunction,
  each,
  has,
} from "underscore";

import { sha1 } from "../fs/watch.js";
import { matches as archMatches } from "../utils/archinfo.js";
import {
  pathJoin,
  pathRelative,
  pathNormalize,
  pathDirname,
  pathBasename,
  convertToOSPath,
  convertToPosixPath,
} from "../fs/files.js";

import LRU from "lru-cache";

import { wrap } from "optimism";
import {
  optimisticStatOrNull,
  optimisticReadJsonOrNull,
} from "../fs/optimistic.js";

const nativeModulesMap = Object.create(null);
const nativeNames = Object.keys(process.binding("natives"));

// Node 0.10 does not include process as a built-in module, but later
// versions of Node do, and we provide a stub for it on the client.
nativeNames.push("process");

nativeNames.forEach(id => {
  if (id.startsWith("internal/")) {
    return;
  }

  // When a native Node module is imported, we register a dependency on a
  // meteor-node-stubs/deps/* module of the same name, so that the
  // necessary stub modules will be included in the bundle. This alternate
  // identifier will not be imported at runtime, but the modules it
  // depends on are necessary for the original import to succeed.
  nativeModulesMap[id] =  "meteor-node-stubs/deps/" + id;
});

const resolverCache = new LRU({
  max: Math.pow(2, 12)
});

export default class Resolver {
  static getOrCreate(options) {
    const key = JSON.stringify(options);
    let resolver = resolverCache.get(key);
    if (! resolver) {
      resolverCache.set(key, resolver = new Resolver(options));
    }
    return resolver;
  }

  constructor({
    sourceRoot,
    targetArch,
    extensions = [".js", ".json"],
    nodeModulesPaths = [],
  }) {
    this.sourceRoot = sourceRoot;
    this.extensions = extensions;
    this.targetArch = targetArch;
    this.nodeModulesPaths = nodeModulesPaths;
    this.statOrNull = optimisticStatOrNull;

    this.resolve = wrap((id, absParentPath) => {
      return this._resolve(id, absParentPath);
    }, {
      makeCacheKey(id, absParentPath) {
        // Only the directory of the absParentPath matters for caching.
        return JSON.stringify([id, pathDirname(absParentPath)]);
      }
    });
  }

  static isTopLevel(id) {
    return "./".indexOf(id.charAt(0)) < 0;
  }

  static isNative(id) {
    return has(nativeModulesMap, id);
  }

  static getNativeStubId(id) {
    return nativeModulesMap[id] || null;
  }

  // Resolve the given module identifier to an object { path, stat } or
  // null, relative to an absolute parent path. The _seenDirPaths
  // parameter is for internal use only and should be ommitted.
  _resolve(id, absParentPath, _seenDirPaths) {
    let resolved =
      this._resolveAbsolute(id, absParentPath) ||
      this._resolveRelative(id, absParentPath) ||
      this._resolveNodeModule(id, absParentPath);

    if (typeof resolved === "string") {
      // The _resolveNodeModule method can return "missing" to indicate
      // that the ImportScanner should look elsewhere for this module,
      // such as in the app node_modules directory.
      return resolved;
    }

    let packageJsonMap = null;

    while (resolved && resolved.stat.isDirectory()) {
      let dirPath = resolved.path;
      _seenDirPaths = _seenDirPaths || new Set;

      // If the "main" field of a package.json file resolves to a
      // directory we've already considered, then we should not attempt to
      // read the same package.json file again.
      if (! _seenDirPaths.has(dirPath)) {
        _seenDirPaths.add(dirPath);
        resolved = this._resolvePkgJsonMain(dirPath, _seenDirPaths);
        if (resolved) {
          // The _resolvePkgJsonMain call above may have returned a
          // directory, so first merge resolved.packageJsonMap into
          // packageJsonMap so that we don't forget the package.json we
          // just resolved, then continue the loop to make sure we fully
          // resolve the "main" module identifier to a non-directory.
          // Technically this could involve even more package.json files,
          // but in practice the "main" property will almost always name a
          // directory containing an index.js file.
          Object.assign(
            packageJsonMap || (packageJsonMap = Object.create(null)),
            resolved.packageJsonMap,
          );
          continue;
        }
      }

      // If we didn't find a `package.json` file, or it didn't have a
      // resolvable `.main` property, the only possibility left to
      // consider is that this directory contains an `index.js` module.
      // This assignment almost always terminates the while loop, because
      // there's very little chance an `index.js` file will be a
      // directory. However, in principle it is remotely possible that a
      // file called `index.js` could be a directory instead of a file.
      resolved = this._joinAndStat(dirPath, "index.js");
    }

    if (resolved) {
      if (packageJsonMap) {
        resolved.packageJsonMap = packageJsonMap;
      }

      // If the package.json file that governs resolved.path has a
      // "browser" field, include it in resolved.packageJsonMap so that
      // the ImportScanner can register the appropriate browser aliases.
      const pkgJsonInfo = this._findPkgJsonSubsetForPath(resolved.path);
      if (pkgJsonInfo &&
          isObject(pkgJsonInfo.pkg.browser)) {
        if (! resolved.packageJsonMap) {
          resolved.packageJsonMap = Object.create(null);
        }
        resolved.packageJsonMap[pkgJsonInfo.path] = pkgJsonInfo.pkg;
      }

      resolved.id = convertToPosixPath(
        convertToOSPath(resolved.path),
        true
      );
    }

    return resolved;
  }

  _joinAndStat(...joinArgs) {
    const joined = pathJoin(...joinArgs);
    const path = pathNormalize(joined);
    const exactStat = this.statOrNull(path);
    const exactResult = exactStat && { path, stat: exactStat };
    let result = null;
    if (exactResult && exactStat.isFile()) {
      result = exactResult;
    }

    if (! result) {
      this.extensions.some(ext => {
        const pathWithExt = path + ext;
        const stat = this.statOrNull(pathWithExt);
        if (stat && ! stat.isDirectory()) {
          return result = { path: pathWithExt, stat };
        }
      });
    }

    if (! result && exactResult && exactStat.isDirectory()) {
      // After trying all available file extensions, fall back to the
      // original result if it was a directory.
      result = exactResult;
    }

    return result;
  }

  _resolveAbsolute(id, absParentPath) {
    return id.charAt(0) === "/" &&
      this._joinAndStat(this.sourceRoot, id.slice(1));
  }

  _resolveRelative(id, absParentPath) {
    if (id.charAt(0) === ".") {
      return this._joinAndStat(absParentPath, "..", id);
    }
  }

  _resolveNodeModule(id, absParentPath) {
    if (! Resolver.isTopLevel(id)) {
      return null;
    }

    if (Resolver.isNative(id) &&
        archMatches(this.targetArch, "os")) {
      // Forbid installing any server module with the same name as a
      // native Node module.
      return null;
    }

    let sourceRoot;
    const relParentPath = pathRelative(this.sourceRoot, absParentPath);
    if (! relParentPath.startsWith("..")) {
      // If the file is contained by this.sourceRoot, then it's safe to
      // use this.sourceRoot as the limiting ancestor directory in the
      // while loop below, but we're still going to check whether the file
      // resides in an external node_modules directory, since "external"
      // .npm/package/node_modules directories are technically contained
      // within the root directory of their packages.
      sourceRoot = this.sourceRoot;
    }

    this.nodeModulesPaths.some(path => {
      if (! pathRelative(path, absParentPath).startsWith("..")) {
        // If the file is inside an external node_modules directory,
        // consider the rootDir to be the parent directory of that
        // node_modules directory, rather than this.sourceRoot.
        return sourceRoot = pathDirname(path);
      }
    });

    let resolved = null;

    if (sourceRoot) {
      let dir = absParentPath; // It's ok for absParentPath to be a directory!
      let info = this._joinAndStat(dir);
      if (! info || ! info.stat.isDirectory()) {
        dir = pathDirname(dir);
      }

      while (! (resolved = this._joinAndStat(dir, "node_modules", id))) {
        if (dir === sourceRoot) {
          break;
        }

        const parentDir = pathDirname(dir);
        if (dir === parentDir) {
          // We've reached the root of the file system??
          break;
        }

        dir = parentDir;
      }
    }

    if (! resolved) {
      // After checking any local node_modules directories, fall back to
      // the package NPM directory, if one was specified.
      this.nodeModulesPaths.some(path => {
        return resolved = this._joinAndStat(path, id);
      });
    }

    // If the dependency is still not resolved, it might be handled by the
    // fallback function defined in meteor/packages/modules/modules.js, or
    // it might be imported in code that will never run on this platform,
    // so there is always the possibility that its absence is not actually
    // a problem. As much as we might like to issue warnings about missing
    // dependencies here, we just don't have enough information to make
    // that determination until the code actually runs.

    return resolved || "missing";
  }

  _resolvePkgJsonMain(dirPath, _seenDirPaths) {
    const found = this._getPkgJsonSubsetForDir(dirPath);
    if (! found) {
      return null;
    }

    if (isString(found.main)) {
      // The "main" field of package.json does not have to begin with ./
      // to be considered relative, so first we try simply appending it to
      // the directory path before falling back to a full resolve, which
      // might return a package from a node_modules directory.
      const resolved = this._joinAndStat(dirPath, found.main) ||
        this._resolve(found.main, found.path, _seenDirPaths);

      if (resolved && typeof resolved === "object") {
        if (! resolved.packageJsonMap) {
          resolved.packageJsonMap = Object.create(null);
        }

        resolved.packageJsonMap[found.path] = found.pkg;

        return resolved;
      }
    }

    return null;
  }

  _getPkgJsonSubsetForDir(dirPath) {
    const pkgJsonPath = pathJoin(dirPath, "package.json");
    const pkg = optimisticReadJsonOrNull(pkgJsonPath);
    if (! pkg) {
      return null;
    }

    // Output a JS module that exports just the "name", "version", "main",
    // and "browser" properties (if defined) from the package.json file.
    const pkgSubset = {};

    if (has(pkg, "name")) {
      pkgSubset.name = pkg.name;
    }

    if (has(pkg, "version")) {
      pkgSubset.version = pkg.version;
    }

    let main;
    function tryMain(name) {
      const value = pkg[name];

      if (isString(value)) {
        main = main || value;
      }

      if (isString(value) ||
          isObject(value)) {
        pkgSubset[name] = value;
      }
    }

    if (archMatches(this.targetArch, "web")) {
      tryMain("browser");
    }

    tryMain("main");

    return {
      path: pkgJsonPath,
      pkg: pkgSubset,
      main,
    };
  }

  _findPkgJsonSubsetForPath(path) {
    const stat = this.statOrNull(path);

    if (stat && stat.isDirectory()) {
      const found = this._getPkgJsonSubsetForDir(path);
      if (found) {
        return found;
      }

      if (path === this.sourceRoot) {
        return null;
      }
    }

    const parentDir = pathDirname(path);

    if (parentDir === path) {
      return null;
    }

    if (pathBasename(parentDir) === "node_modules") {
      return null;
    }

    return this._findPkgJsonSubsetForPath(parentDir);
  }
};

import { Profile } from "../tool-env/profile.js";
each(Resolver.prototype, (value, key) => {
  if (key === "constructor") return;
  Resolver.prototype[key] = Profile(
    `Resolver#${key}`,
    Resolver.prototype[key]
  );
});
<|endoftext|>


<commit_before>"use strict";

const _ = require("lodash");
const path = require("path");
const { createFsFromVolume, Volume } = require("memfs");
const webpack = require("..");
const captureStdio = require("./helpers/captureStdio");

const createMultiCompiler = (progressOptions, configOptions) => {
	const compiler = webpack(
		Object.assign(
			[
				{
					context: path.join(__dirname, "fixtures"),
					entry: "./a.js"
				},
				{
					context: path.join(__dirname, "fixtures"),
					entry: "./b.js"
				}
			],
			configOptions
		)
	);
	compiler.outputFileSystem = createFsFromVolume(new Volume());

	new webpack.ProgressPlugin(progressOptions).apply(compiler);

	return compiler;
};

const createSimpleCompiler = progressOptions => {
	const compiler = webpack({
		context: path.join(__dirname, "fixtures"),
		entry: "./a.js",
		infrastructureLogging: {
			debug: /Progress/
		}
	});

	compiler.outputFileSystem = createFsFromVolume(new Volume());

	new webpack.ProgressPlugin({
		activeModules: true,
		...progressOptions
	}).apply(compiler);

	return compiler;
};

const createSimpleCompilerWithCustomHandler = options => {
	const compiler = webpack({
		context: path.join(__dirname, "fixtures"),
		entry: "./a.js"
	});

	compiler.outputFileSystem = createFsFromVolume(new Volume());
	const logger = compiler.getInfrastructureLogger("custom test logger");
	new webpack.ProgressPlugin({
		activeModules: true,
		...options,
		handler: (...args) => logger.status(args)
	}).apply(compiler);

	return compiler;
};

const getLogs = logsStr => logsStr.split(/\r/).filter(v => !(v === " "));

const RunCompilerAsync = compiler =>
	new Promise((resolve, reject) => {
		compiler.run(err => {
			if (err) {
				reject(err);
			} else {
				resolve();
			}
		});
	});

describe("ProgressPlugin", function () {
	let stderr;
	let stdout;

	beforeEach(() => {
		stderr = captureStdio(process.stderr, true);
		stdout = captureStdio(process.stdout, true);
	});
	afterEach(() => {
		stderr && stderr.restore();
		stdout && stdout.restore();
	});

	const nanTest = createCompiler => () => {
		const compiler = createCompiler();

		return RunCompilerAsync(compiler).then(() => {
			expect(stderr.toString()).toContain("%");
			expect(stderr.toString()).not.toContain("NaN");
		});
	};

	it(
		"should not contain NaN as a percentage when it is applied to Compiler",
		nanTest(createSimpleCompiler)
	);
	it(
		"should not contain NaN as a percentage when it is applied to MultiCompiler",
		nanTest(createMultiCompiler)
	);
	it(
		"should not contain NaN as a percentage when it is applied to MultiCompiler (paralellism: 1)",
		nanTest(() => createMultiCompiler(undefined, { parallelism: 1 }))
	);

	it("should print profile information", () => {
		const compiler = createSimpleCompiler({
			profile: true
		});

		return RunCompilerAsync(compiler).then(() => {
			const logs = getLogs(stderr.toString());

			expect(logs).toContainEqual(
				expect.stringMatching(
					/\[webpack\.Progress\] {2}| {2}| \d+ ms module ids > DeterministicModuleIdsPlugin\n$/
				)
			);
			expect(logs).toContainEqual(
				expect.stringMatching(
					/\[webpack\.Progress\] {2}| \d+ ms building > \.\.\. entries \.\.\. dependencies \.\.\. modules\n$/
				)
			);
			expect(logs).toContainEqual(
				expect.stringMatching(/\[webpack\.Progress\] \d+ ms building\n$/)
			);
			expect(logs).toContainEqual(
				expect.stringMatching(
					/\[webpack\.Progress\] {2}| \d+ ms sealing > module ids\n$/
				)
			);
			expect(logs).toContainEqual(
				expect.stringMatching(/\[webpack\.Progress\] \d+ ms sealing\n$/)
			);
		});
	});

	const monotonicTest = createCompiler => () => {
		const handlerCalls = [];
		const compiler = createCompiler({
			handler: (p, ...args) => {
				handlerCalls.push({ value: p, text: `${p}% ${args.join(" ")}` });
			}
		});

		return RunCompilerAsync(compiler).then(() => {
			let lastLine = handlerCalls[0];
			for (const line of handlerCalls) {
				if (line.value < lastLine.value) {
					throw new Error(
						`Progress value is not monotonic increasing:\n${lastLine.text}\n${line.text}`
					);
				}
				lastLine = line;
			}
		});
	};

	it(
		"should have monotonic increasing progress",
		monotonicTest(createSimpleCompiler)
	);
	it(
		"should have monotonic increasing progress (multi compiler)",
		monotonicTest(createMultiCompiler)
	);
	it(
		"should have monotonic increasing progress (multi compiler, parallelism)",
		monotonicTest(o => createMultiCompiler(o, { parallelism: 1 }))
	);

	it("should not print lines longer than stderr.columns", () => {
		const compiler = createSimpleCompiler();
		process.stderr.columns = 36;

		return RunCompilerAsync(compiler).then(() => {
			const logs = getLogs(stderr.toString());

			expect(logs.length).toBeGreaterThan(20);
			logs.forEach(log => expect(log.length).toBeLessThanOrEqual(35));
			expect(logs).toContain(
				"75% sealing ...mization ...nsPlugin",
				"trims each detail string equally"
			);
			expect(logs).toContain("92% sealing asset processing");
			expect(logs).toContain("100%");
		});
	});

	it("should handle when stderr.columns is undefined", () => {
		const compiler = createSimpleCompiler();

		process.stderr.columns = undefined;
		return RunCompilerAsync(compiler).then(() => {
			const logs = getLogs(stderr.toString());

			expect(logs.length).toBeGreaterThan(20);
			expect(_.maxBy(logs, "length").length).toBeGreaterThan(50);
		});
	});

	it("should contain the new compiler hooks", () => {
		const compiler = createSimpleCompiler();

		process.stderr.columns = undefined;
		return RunCompilerAsync(compiler).then(() => {
			const logs = getLogs(stderr.toString());

			expect(logs).toContain("4% setup normal module factory");
			expect(logs).toContain("5% setup context module factory");
		});
	});

	it("should display all type of percentage when it is applied to SingleCompiler", () => {
		const compiler = createSimpleCompiler({
			entries: true,
			modules: true,
			dependencies: true,
			activeModules: true
		});

		return RunCompilerAsync(compiler).then(() => {
			const logs = stderr.toString();

			expect(logs).toEqual(expect.stringMatching(/\d+\/\d+ entries/));
			expect(logs).toEqual(expect.stringMatching(/\d+\/\d+ dependencies/));
			expect(logs).toEqual(expect.stringMatching(/\d+\/\d+ modules/));
			expect(logs).toEqual(expect.stringMatching(/\d+ active/));
		});
	});

	it("should get the custom handler text from the log", () => {
		const compiler = createSimpleCompilerWithCustomHandler();

		return RunCompilerAsync(compiler).then(() => {
			const logs = stderr.toString();
			expect(logs).toEqual(
				expect.stringMatching(/\d+\/\d+ [custom test logger]/)
			);
			expect(logs).toEqual(expect.stringMatching(/\d+ active/));
			expect(logs).toEqual(expect.stringMatching(/\d+\/\d+ modules/));
		});
	});
});
<commit_msg>increase timeout for ProgressPlugin test<commit_after>"use strict";

const _ = require("lodash");
const path = require("path");
const { createFsFromVolume, Volume } = require("memfs");
const webpack = require("..");
const captureStdio = require("./helpers/captureStdio");

const createMultiCompiler = (progressOptions, configOptions) => {
	const compiler = webpack(
		Object.assign(
			[
				{
					context: path.join(__dirname, "fixtures"),
					entry: "./a.js"
				},
				{
					context: path.join(__dirname, "fixtures"),
					entry: "./b.js"
				}
			],
			configOptions
		)
	);
	compiler.outputFileSystem = createFsFromVolume(new Volume());

	new webpack.ProgressPlugin(progressOptions).apply(compiler);

	return compiler;
};

const createSimpleCompiler = progressOptions => {
	const compiler = webpack({
		context: path.join(__dirname, "fixtures"),
		entry: "./a.js",
		infrastructureLogging: {
			debug: /Progress/
		}
	});

	compiler.outputFileSystem = createFsFromVolume(new Volume());

	new webpack.ProgressPlugin({
		activeModules: true,
		...progressOptions
	}).apply(compiler);

	return compiler;
};

const createSimpleCompilerWithCustomHandler = options => {
	const compiler = webpack({
		context: path.join(__dirname, "fixtures"),
		entry: "./a.js"
	});

	compiler.outputFileSystem = createFsFromVolume(new Volume());
	const logger = compiler.getInfrastructureLogger("custom test logger");
	new webpack.ProgressPlugin({
		activeModules: true,
		...options,
		handler: (...args) => logger.status(args)
	}).apply(compiler);

	return compiler;
};

const getLogs = logsStr => logsStr.split(/\r/).filter(v => !(v === " "));

const RunCompilerAsync = compiler =>
	new Promise((resolve, reject) => {
		compiler.run(err => {
			if (err) {
				reject(err);
			} else {
				resolve();
			}
		});
	});

describe("ProgressPlugin", function () {
	jest.setTimeout(10000);
	let stderr;
	let stdout;

	beforeEach(() => {
		stderr = captureStdio(process.stderr, true);
		stdout = captureStdio(process.stdout, true);
	});
	afterEach(() => {
		stderr && stderr.restore();
		stdout && stdout.restore();
	});

	const nanTest = createCompiler => () => {
		const compiler = createCompiler();

		return RunCompilerAsync(compiler).then(() => {
			expect(stderr.toString()).toContain("%");
			expect(stderr.toString()).not.toContain("NaN");
		});
	};

	it(
		"should not contain NaN as a percentage when it is applied to Compiler",
		nanTest(createSimpleCompiler)
	);
	it(
		"should not contain NaN as a percentage when it is applied to MultiCompiler",
		nanTest(createMultiCompiler)
	);
	it(
		"should not contain NaN as a percentage when it is applied to MultiCompiler (paralellism: 1)",
		nanTest(() => createMultiCompiler(undefined, { parallelism: 1 }))
	);

	it("should print profile information", () => {
		const compiler = createSimpleCompiler({
			profile: true
		});

		return RunCompilerAsync(compiler).then(() => {
			const logs = getLogs(stderr.toString());

			expect(logs).toContainEqual(
				expect.stringMatching(
					/\[webpack\.Progress\] {2}| {2}| \d+ ms module ids > DeterministicModuleIdsPlugin\n$/
				)
			);
			expect(logs).toContainEqual(
				expect.stringMatching(
					/\[webpack\.Progress\] {2}| \d+ ms building > \.\.\. entries \.\.\. dependencies \.\.\. modules\n$/
				)
			);
			expect(logs).toContainEqual(
				expect.stringMatching(/\[webpack\.Progress\] \d+ ms building\n$/)
			);
			expect(logs).toContainEqual(
				expect.stringMatching(
					/\[webpack\.Progress\] {2}| \d+ ms sealing > module ids\n$/
				)
			);
			expect(logs).toContainEqual(
				expect.stringMatching(/\[webpack\.Progress\] \d+ ms sealing\n$/)
			);
		});
	});

	const monotonicTest = createCompiler => () => {
		const handlerCalls = [];
		const compiler = createCompiler({
			handler: (p, ...args) => {
				handlerCalls.push({ value: p, text: `${p}% ${args.join(" ")}` });
			}
		});

		return RunCompilerAsync(compiler).then(() => {
			let lastLine = handlerCalls[0];
			for (const line of handlerCalls) {
				if (line.value < lastLine.value) {
					throw new Error(
						`Progress value is not monotonic increasing:\n${lastLine.text}\n${line.text}`
					);
				}
				lastLine = line;
			}
		});
	};

	it(
		"should have monotonic increasing progress",
		monotonicTest(createSimpleCompiler)
	);
	it(
		"should have monotonic increasing progress (multi compiler)",
		monotonicTest(createMultiCompiler)
	);
	it(
		"should have monotonic increasing progress (multi compiler, parallelism)",
		monotonicTest(o => createMultiCompiler(o, { parallelism: 1 }))
	);

	it("should not print lines longer than stderr.columns", () => {
		const compiler = createSimpleCompiler();
		process.stderr.columns = 36;

		return RunCompilerAsync(compiler).then(() => {
			const logs = getLogs(stderr.toString());

			expect(logs.length).toBeGreaterThan(20);
			logs.forEach(log => expect(log.length).toBeLessThanOrEqual(35));
			expect(logs).toContain(
				"75% sealing ...mization ...nsPlugin",
				"trims each detail string equally"
			);
			expect(logs).toContain("92% sealing asset processing");
			expect(logs).toContain("100%");
		});
	});

	it("should handle when stderr.columns is undefined", () => {
		const compiler = createSimpleCompiler();

		process.stderr.columns = undefined;
		return RunCompilerAsync(compiler).then(() => {
			const logs = getLogs(stderr.toString());

			expect(logs.length).toBeGreaterThan(20);
			expect(_.maxBy(logs, "length").length).toBeGreaterThan(50);
		});
	});

	it("should contain the new compiler hooks", () => {
		const compiler = createSimpleCompiler();

		process.stderr.columns = undefined;
		return RunCompilerAsync(compiler).then(() => {
			const logs = getLogs(stderr.toString());

			expect(logs).toContain("4% setup normal module factory");
			expect(logs).toContain("5% setup context module factory");
		});
	});

	it("should display all type of percentage when it is applied to SingleCompiler", () => {
		const compiler = createSimpleCompiler({
			entries: true,
			modules: true,
			dependencies: true,
			activeModules: true
		});

		return RunCompilerAsync(compiler).then(() => {
			const logs = stderr.toString();

			expect(logs).toEqual(expect.stringMatching(/\d+\/\d+ entries/));
			expect(logs).toEqual(expect.stringMatching(/\d+\/\d+ dependencies/));
			expect(logs).toEqual(expect.stringMatching(/\d+\/\d+ modules/));
			expect(logs).toEqual(expect.stringMatching(/\d+ active/));
		});
	});

	it("should get the custom handler text from the log", () => {
		const compiler = createSimpleCompilerWithCustomHandler();

		return RunCompilerAsync(compiler).then(() => {
			const logs = stderr.toString();
			expect(logs).toEqual(
				expect.stringMatching(/\d+\/\d+ [custom test logger]/)
			);
			expect(logs).toEqual(expect.stringMatching(/\d+ active/));
			expect(logs).toEqual(expect.stringMatching(/\d+\/\d+ modules/));
		});
	});
});
<|endoftext|>


<commit_before>/**
 * Checkout.com
 * Authorized and regulated as an electronic money institution
 * by the UK Financial Conduct Authority (FCA) under number 900816.
 *
 * PHP version 7
 *
 * @category  Magento2
 * @package   Checkout.com
 * @author    Platforms Development Team <platforms@checkout.com>
 * @copyright 2010-2019 Checkout.com
 * @license   https://opensource.org/licenses/mit-license.html MIT License
 * @link      https://docs.checkout.com/
 */

define(
    [
        'jquery',
        'CheckoutCom_Magento2/js/view/payment/config-loader',
        'Magento_Checkout/js/model/quote',
        'Magento_Checkout/js/checkout-data',
        'mage/url',
        'Magento_Checkout/js/action/redirect-on-success',
        'Magento_Checkout/js/model/full-screen-loader',
        'Magento_Customer/js/model/customer',
        'mage/translate',
        'mage/cookies'
    ],
    function ($, Config, Quote, CheckoutData, Url, RedirectOnSuccessAction, FullScreenLoader, Customer, __) {
        'use strict';

        const KEY_CONFIG = 'checkoutcom_configuration';
        const KEY_DATA = 'checkoutcom_data';

        return {
            /**
             * Gets a field value.
             *
             * @param  {string}  methodId The method id
             * @param  {string}  field    The field
             * @return {mixed}            The value
             */
            getValue: function (methodId, field, strict= false) {
                var val = null;
                if (methodId && Config.hasOwnProperty(methodId) && Config[methodId].hasOwnProperty(field)) {
                    val = Config[methodId][field]
                } else if (Config.hasOwnProperty(KEY_CONFIG) && Config[KEY_CONFIG].hasOwnProperty(field) && !strict) {
                    val = Config[KEY_CONFIG][field];
                }

                return val;
            },

            /**
             * Load a CSS file
             *
             * @return {void}
             */
            loadCss: function (fileName, folderPath) {
                // Prepare the folder path
                folderPath = (folderPath) ? '/' + folderPath : '';

                // Get the CSS config parameters
                var useMinCss = window.checkoutConfig.payment.checkoutcom_magento2.checkoutcom_data.use_minified_css;
                var ext = (useMinCss == '1') ? '.min.css' : '.css';

                // Build the payment form CSS path
                var cssPath = window.checkoutConfig.payment.checkoutcom_magento2.checkoutcom_data.css_path;
                cssPath += folderPath + '/' + fileName + ext;

                // Append the CSS file
                $('head').append('<link rel="stylesheet" href="' + cssPath + '" type="text/css"/>');
            },

            /**
             * Load a remote JS file
             *
             * @return {void}
             */
            loadRemoteJs: function (jsUrl) {
                // Append the CSS file
                $('head').append('');
            },

            /**
             * Get the store name.
             *
             * @return {string}  The store name.
             */
            getStoreName: function () {
                return Config[KEY_DATA].store.name;
            },

            /**
             * Get the quote value.
             *
             * @return {float}  The quote value.
             */
            getQuoteValue: function () {
                var data = this.getRestQuoteData();
                var amount = parseFloat(data.totals.base_grand_total);

                return amount.toFixed(2);
            },

            /**
             * Get the updated quote data from the core REST API.
             *
             * @return {object}  The quote data.
             */
            getRestQuoteData: function () {
                // Prepare the required parameters
                var self = this;
                var result = null;
                var restUrl = window.BASE_URL;

                // Build the rest URL
                if (Customer.isLoggedIn()) {
                    restUrl += 'rest/default/V1/';
                    restUrl += 'carts/mine/payment-information';
                    restUrl += '?form_key=' + window.checkoutConfig.formKey;
                }
                else {
                    restUrl += 'rest/all/V1/guest-carts/' + window.checkoutConfig.quoteData.entity_id + '/payment-information';
                    restUrl += '?form_key=' + window.checkoutConfig.formKey;
                }

                // Set the event to update data on any button click
                $('button[type="submit"]')
                .off('click', self.getRestQuoteData)
                .on('click', self.getRestQuoteData);

                    // Send the AJAX request
                    $.ajax({
                        url: restUrl,
                        type: 'GET',
                        contentType: "application/json",
                        dataType: "json",
                        async: false,
                        showLoader: true,
                        success: function (data, status, xhr) {
                            result = data;
                        },
                        error: function (request, status, error) {
                            self.log(error);
                        }
                    });

                return result;
            },

            /**
             * Get the quote currency.
             *
             * @return {string}  The quote currency.
             */
            getQuoteCurrency: function () {
                return Config[KEY_DATA].quote.currency;
            },

            /**
             * Check if the payment method should be enabled by default.
             *
             * @return {void}
             */
            checkDefaultEnabled: function (methodId) {
                if (this.getValue(null, 'default_method') == methodId) {
                    $('input#' + methodId).trigger('click');
                }
            },

            /**
             * Checks if user has saved cards.
             *
             * @return {bool}
             */
            userHasCards: function () {
                return Config[KEY_DATA].user.has_cards;
            },

            /**
             * Gets the card form language fallback.
             *
             * @return {string}
             */
            getLangageFallback: function () {
                return Config[KEY_DATA].user.language_fallback;
            },

            /**
             * Get the card form shop language
             *
             * @return {string}
             */
            getShopLanguage() {
                let mageShopLanguage = Config[KEY_DATA].store.language;
                let framesLanguage;
                switch(mageShopLanguage) {
                    case 'nl_NL':
                        framesLanguage = 'NL-NL';
                        break;
                    case 'en_GB':
                         framesLanguage = 'EN-GB';
                        break;
                    case 'fr_FR':
                         framesLanguage = 'FR-FR';
                        break;
                    case 'de_DE':
                        framesLanguage = 'DE-DE';
                        break;
                    case 'it_IT':
                        framesLanguage = 'IT-IT';
                        break;
                    case 'ko_KR':
                        framesLanguage = 'KR-KR';
                        break;
                    case 'es_ES':
                        framesLanguage = 'ES-ES';
                        break;
                    default:
                      framesLanguage = this.getLangageFallback().toUpperCase().split("_").join('-');
                }

                return framesLanguage;
            },

            /**
             * Get the supported cards.
             *
             * @return {array}
             */
            getSupportedCards: function () {
                return Config[KEY_DATA].cards;
            },

            /**
             * Builds the controller URL.
             *
             * @param  {string}  path  The path
             * @return {string}
             */
            getUrl: function (path) {
                return Url.build('checkout_com/' + path);
            },

            /**
             * Customer name.
             *
             * @param  {bool} return in object format.
             * @return {mixed}  The billing address.
             */
            getCustomerName: function () {
                var billingAddress = Quote.billingAddress();
                var customerName = '';
                if (billingAddress) {
                    customerName += billingAddress.firstname;
                    customerName += ' ' + billingAddress.lastname;
                }

                return customerName;
            },

            /**
             * Billing address.
             *
             * @return {object}  The billing address.
             */
            getBillingAddress: function () {
                return Quote.billingAddress();
            },

            /**
             * @return {string}
             */
            getEmail: function () {
                var emailCookieName = this.getValue(null, 'email_cookie_name');
                return window.checkoutConfig.customerData.email
                || Quote.guestEmail
                || CheckoutData.getValidatedEmailValue()
                || $.cookie(emailCookieName);
            },

            /**
             * @return {void}
             */
            setEmail: function () {
                var userEmail = this.getEmail();
                var emailCookieName = this.getValue(null, 'email_cookie_name');
                $.cookie(emailCookieName, userEmail);

                // If no email found, observe the core email field
                if (!userEmail) {
                    $('#customer-email').off('change').on('change', function () {
                        userEmail = Quote.guestEmail || CheckoutData.getValidatedEmailValue();
                        $.cookie(emailCookieName, userEmail);
                    });
                }
            },

            /**
             * @return {object}
             */
            getPhone: function () {
                var billingAddress = Quote.billingAddress();

                return {
                    number: billingAddress.telephone
                };
            },

            /**
             * Handle error logging.
             */
            log: function (val) {
                if (this.getValue(null, 'debug')
                    && this.getValue(null, 'console_logging')
                ) {
                    console.log(val);
                }
            },

            /**
             * Show a message.
             */
            showMessage: function (type, message, methodId) {
                this.clearMessages(methodId);
                var messageContainer = this.getMethodContainer(methodId).find('.message-cko');
                messageContainer.addClass('message-' + type + ' ' + type);
                messageContainer.append('
' + __(message) + '
');
                messageContainer.show();
            },

            /**
             * Show debug message.
             */
            showDebugMessage: function (type, message, methodId) {
                var messageContainer = this.getMethodContainer(methodId).find('.debug-message');
                messageContainer.addClass('message-' + type + ' ' + type);
                messageContainer.append('

' + __(message) + '

');
                messageContainer.show();
            },

            /**
             * Show response code.
             */
            showResponseCode: function (type, message, methodId) {
                var messageContainer = this.getMethodContainer(methodId).find('.message-response-code');
                messageContainer.addClass('message-' + type + ' ' + type);
                messageContainer.append('
Response Code: ' + __(message) + '
');
                messageContainer.show();
            },

            /**
             * Clear all messages.
             */
            clearMessages: function (methodId) {
                var messageContainer = this.getMethodContainer(methodId).find('.message');
                messageContainer.hide();
                messageContainer.empty();
            },

            /**
             * Get a payment method container instance.
             */
            getMethodContainer: function (methodId) {
                return $('#' + methodId + '_container');
            },

            /**
             * Check if an URL is valid.
             */
            isUrl: function (str) {
                var pattern = /^(?:\w+:)?\/\/([^\s\.]+\.\S{2}|localhost[\:?\d]*)\S*$/;
                return pattern.test(str);
            },

            /**
             * Handle the place order button state.
             */
            allowPlaceOrder: function (buttonId, yesNo) {
                $('#' + buttonId).prop('disabled', !yesNo);
            },

            /**
             * Check if a payment option is active.
             */
            methodIsSelected: function (idSelector) {
                var id = idSelector.replace('#', '');
                var selected = CheckoutData.getSelectedPaymentMethod();
                return id == selected || selected == null;
            },

            /**
             * Place a new order.
             *
             * @return {void}
             */
            placeOrder: function (payload, methodId) {
                var self = this;

                // Start the loader
                FullScreenLoader.startLoader();

                // Send the request
                $.ajax({
                    type: 'POST',
                    url: self.getUrl('payment/placeorder'),
                    data: payload,
                    success: function (data) {
                        if (!data.success) {
                            FullScreenLoader.stopLoader();
                            self.showMessage('error', data.message, methodId);
                            if (data.debugMessage) {
                                self.showDebugMessage('error', data.debugMessage, methodId);
                            }
                            if (data.responseCode) {
                                self.showResponseCode('error', data.responseCode, methodId);
                            }
                            self.allowPlaceOrder(methodId + '_btn', false)
                        } else if (data.success && data.url) {
                            // Handle 3DS redirection
                            window.location.href = data.url;
                        } else {
                            // Normal redirection
                            RedirectOnSuccessAction.execute();
                        }
                    },
                    error: function (request, status, error) {
                        self.showMessage('error', error, methodId);
                        FullScreenLoader.stopLoader();
                    }
                });
            }
        };
    }
);<commit_msg>Fixed syntax<commit_after>/**
 * Checkout.com
 * Authorized and regulated as an electronic money institution
 * by the UK Financial Conduct Authority (FCA) under number 900816.
 *
 * PHP version 7
 *
 * @category  Magento2
 * @package   Checkout.com
 * @author    Platforms Development Team <platforms@checkout.com>
 * @copyright 2010-2019 Checkout.com
 * @license   https://opensource.org/licenses/mit-license.html MIT License
 * @link      https://docs.checkout.com/
 */

define(
    [
        'jquery',
        'CheckoutCom_Magento2/js/view/payment/config-loader',
        'Magento_Checkout/js/model/quote',
        'Magento_Checkout/js/checkout-data',
        'mage/url',
        'Magento_Checkout/js/action/redirect-on-success',
        'Magento_Checkout/js/model/full-screen-loader',
        'Magento_Customer/js/model/customer',
        'mage/translate',
        'mage/cookies'
    ],
    function ($, Config, Quote, CheckoutData, Url, RedirectOnSuccessAction, FullScreenLoader, Customer, __) {
        'use strict';

        const KEY_CONFIG = 'checkoutcom_configuration';
        const KEY_DATA = 'checkoutcom_data';

        return {
            /**
             * Gets a field value.
             *
             * @param  {string}  methodId The method id
             * @param  {string}  field    The field
             * @return {mixed}            The value
             */
            getValue: function (methodId, field, strict = false) {
                var val = null;
                if (methodId && Config.hasOwnProperty(methodId) && Config[methodId].hasOwnProperty(field)) {
                    val = Config[methodId][field]
                } else if (Config.hasOwnProperty(KEY_CONFIG) && Config[KEY_CONFIG].hasOwnProperty(field) && !strict) {
                    val = Config[KEY_CONFIG][field];
                }

                return val;
            },

            /**
             * Load a CSS file
             *
             * @return {void}
             */
            loadCss: function (fileName, folderPath) {
                // Prepare the folder path
                folderPath = (folderPath) ? '/' + folderPath : '';

                // Get the CSS config parameters
                var useMinCss = window.checkoutConfig.payment.checkoutcom_magento2.checkoutcom_data.use_minified_css;
                var ext = (useMinCss == '1') ? '.min.css' : '.css';

                // Build the payment form CSS path
                var cssPath = window.checkoutConfig.payment.checkoutcom_magento2.checkoutcom_data.css_path;
                cssPath += folderPath + '/' + fileName + ext;

                // Append the CSS file
                $('head').append('<link rel="stylesheet" href="' + cssPath + '" type="text/css"/>');
            },

            /**
             * Load a remote JS file
             *
             * @return {void}
             */
            loadRemoteJs: function (jsUrl) {
                // Append the CSS file
                $('head').append('');
            },

            /**
             * Get the store name.
             *
             * @return {string}  The store name.
             */
            getStoreName: function () {
                return Config[KEY_DATA].store.name;
            },

            /**
             * Get the quote value.
             *
             * @return {float}  The quote value.
             */
            getQuoteValue: function () {
                var data = this.getRestQuoteData();
                var amount = parseFloat(data.totals.base_grand_total);

                return amount.toFixed(2);
            },

            /**
             * Get the updated quote data from the core REST API.
             *
             * @return {object}  The quote data.
             */
            getRestQuoteData: function () {
                // Prepare the required parameters
                var self = this;
                var result = null;
                var restUrl = window.BASE_URL;

                // Build the rest URL
                if (Customer.isLoggedIn()) {
                    restUrl += 'rest/default/V1/';
                    restUrl += 'carts/mine/payment-information';
                    restUrl += '?form_key=' + window.checkoutConfig.formKey;
                }
                else {
                    restUrl += 'rest/all/V1/guest-carts/' + window.checkoutConfig.quoteData.entity_id + '/payment-information';
                    restUrl += '?form_key=' + window.checkoutConfig.formKey;
                }

                // Set the event to update data on any button click
                $('button[type="submit"]')
                .off('click', self.getRestQuoteData)
                .on('click', self.getRestQuoteData);

                    // Send the AJAX request
                    $.ajax({
                        url: restUrl,
                        type: 'GET',
                        contentType: "application/json",
                        dataType: "json",
                        async: false,
                        showLoader: true,
                        success: function (data, status, xhr) {
                            result = data;
                        },
                        error: function (request, status, error) {
                            self.log(error);
                        }
                    });

                return result;
            },

            /**
             * Get the quote currency.
             *
             * @return {string}  The quote currency.
             */
            getQuoteCurrency: function () {
                return Config[KEY_DATA].quote.currency;
            },

            /**
             * Check if the payment method should be enabled by default.
             *
             * @return {void}
             */
            checkDefaultEnabled: function (methodId) {
                if (this.getValue(null, 'default_method') == methodId) {
                    $('input#' + methodId).trigger('click');
                }
            },

            /**
             * Checks if user has saved cards.
             *
             * @return {bool}
             */
            userHasCards: function () {
                return Config[KEY_DATA].user.has_cards;
            },

            /**
             * Gets the card form language fallback.
             *
             * @return {string}
             */
            getLangageFallback: function () {
                return Config[KEY_DATA].user.language_fallback;
            },

            /**
             * Get the card form shop language
             *
             * @return {string}
             */
            getShopLanguage() {
                let mageShopLanguage = Config[KEY_DATA].store.language;
                let framesLanguage;
                switch(mageShopLanguage) {
                    case 'nl_NL':
                        framesLanguage = 'NL-NL';
                        break;
                    case 'en_GB':
                         framesLanguage = 'EN-GB';
                        break;
                    case 'fr_FR':
                         framesLanguage = 'FR-FR';
                        break;
                    case 'de_DE':
                        framesLanguage = 'DE-DE';
                        break;
                    case 'it_IT':
                        framesLanguage = 'IT-IT';
                        break;
                    case 'ko_KR':
                        framesLanguage = 'KR-KR';
                        break;
                    case 'es_ES':
                        framesLanguage = 'ES-ES';
                        break;
                    default:
                      framesLanguage = this.getLangageFallback().toUpperCase().split("_").join('-');
                }

                return framesLanguage;
            },

            /**
             * Get the supported cards.
             *
             * @return {array}
             */
            getSupportedCards: function () {
                return Config[KEY_DATA].cards;
            },

            /**
             * Builds the controller URL.
             *
             * @param  {string}  path  The path
             * @return {string}
             */
            getUrl: function (path) {
                return Url.build('checkout_com/' + path);
            },

            /**
             * Customer name.
             *
             * @param  {bool} return in object format.
             * @return {mixed}  The billing address.
             */
            getCustomerName: function () {
                var billingAddress = Quote.billingAddress();
                var customerName = '';
                if (billingAddress) {
                    customerName += billingAddress.firstname;
                    customerName += ' ' + billingAddress.lastname;
                }

                return customerName;
            },

            /**
             * Billing address.
             *
             * @return {object}  The billing address.
             */
            getBillingAddress: function () {
                return Quote.billingAddress();
            },

            /**
             * @return {string}
             */
            getEmail: function () {
                var emailCookieName = this.getValue(null, 'email_cookie_name');
                return window.checkoutConfig.customerData.email
                || Quote.guestEmail
                || CheckoutData.getValidatedEmailValue()
                || $.cookie(emailCookieName);
            },

            /**
             * @return {void}
             */
            setEmail: function () {
                var userEmail = this.getEmail();
                var emailCookieName = this.getValue(null, 'email_cookie_name');
                $.cookie(emailCookieName, userEmail);

                // If no email found, observe the core email field
                if (!userEmail) {
                    $('#customer-email').off('change').on('change', function () {
                        userEmail = Quote.guestEmail || CheckoutData.getValidatedEmailValue();
                        $.cookie(emailCookieName, userEmail);
                    });
                }
            },

            /**
             * @return {object}
             */
            getPhone: function () {
                var billingAddress = Quote.billingAddress();

                return {
                    number: billingAddress.telephone
                };
            },

            /**
             * Handle error logging.
             */
            log: function (val) {
                if (this.getValue(null, 'debug')
                    && this.getValue(null, 'console_logging')
                ) {
                    console.log(val);
                }
            },

            /**
             * Show a message.
             */
            showMessage: function (type, message, methodId) {
                this.clearMessages(methodId);
                var messageContainer = this.getMethodContainer(methodId).find('.message-cko');
                messageContainer.addClass('message-' + type + ' ' + type);
                messageContainer.append('
' + __(message) + '
');
                messageContainer.show();
            },

            /**
             * Show debug message.
             */
            showDebugMessage: function (type, message, methodId) {
                var messageContainer = this.getMethodContainer(methodId).find('.debug-message');
                messageContainer.addClass('message-' + type + ' ' + type);
                messageContainer.append('

' + __(message) + '

');
                messageContainer.show();
            },

            /**
             * Show response code.
             */
            showResponseCode: function (type, message, methodId) {
                var messageContainer = this.getMethodContainer(methodId).find('.message-response-code');
                messageContainer.addClass('message-' + type + ' ' + type);
                messageContainer.append('
Response Code: ' + __(message) + '
');
                messageContainer.show();
            },

            /**
             * Clear all messages.
             */
            clearMessages: function (methodId) {
                var messageContainer = this.getMethodContainer(methodId).find('.message');
                messageContainer.hide();
                messageContainer.empty();
            },

            /**
             * Get a payment method container instance.
             */
            getMethodContainer: function (methodId) {
                return $('#' + methodId + '_container');
            },

            /**
             * Check if an URL is valid.
             */
            isUrl: function (str) {
                var pattern = /^(?:\w+:)?\/\/([^\s\.]+\.\S{2}|localhost[\:?\d]*)\S*$/;
                return pattern.test(str);
            },

            /**
             * Handle the place order button state.
             */
            allowPlaceOrder: function (buttonId, yesNo) {
                $('#' + buttonId).prop('disabled', !yesNo);
            },

            /**
             * Check if a payment option is active.
             */
            methodIsSelected: function (idSelector) {
                var id = idSelector.replace('#', '');
                var selected = CheckoutData.getSelectedPaymentMethod();
                return id == selected || selected == null;
            },

            /**
             * Place a new order.
             *
             * @return {void}
             */
            placeOrder: function (payload, methodId) {
                var self = this;

                // Start the loader
                FullScreenLoader.startLoader();

                // Send the request
                $.ajax({
                    type: 'POST',
                    url: self.getUrl('payment/placeorder'),
                    data: payload,
                    success: function (data) {
                        if (!data.success) {
                            FullScreenLoader.stopLoader();
                            self.showMessage('error', data.message, methodId);
                            if (data.debugMessage) {
                                self.showDebugMessage('error', data.debugMessage, methodId);
                            }
                            if (data.responseCode) {
                                self.showResponseCode('error', data.responseCode, methodId);
                            }
                            self.allowPlaceOrder(methodId + '_btn', false)
                        } else if (data.success && data.url) {
                            // Handle 3DS redirection
                            window.location.href = data.url;
                        } else {
                            // Normal redirection
                            RedirectOnSuccessAction.execute();
                        }
                    },
                    error: function (request, status, error) {
                        self.showMessage('error', error, methodId);
                        FullScreenLoader.stopLoader();
                    }
                });
            }
        };
    }
);<|endoftext|>


<commit_before>import { gql } from "@apollo/client"
import { SEARCH_OBJECT_TYPES, setSearchQuery } from "actions"
import API from "api"
import ConfirmDestructive from "components/ConfirmDestructive"
import Fieldset from "components/Fieldset"
import Messages from "components/Messages"
import {
  jumpToTop,
  mapPageDispatchersToProps,
  PageDispatchersPropType,
  useBoilerplate
} from "components/Page"
import SavedSearchTable from "components/SavedSearchTable"
import { deserializeQueryParams } from "components/SearchFilters"
import _isEmpty from "lodash/isEmpty"
import PropTypes from "prop-types"
import React, { useState } from "react"
import { Button, ControlLabel, FormControl, FormGroup } from "react-bootstrap"
import { connect } from "react-redux"
import { useHistory } from "react-router-dom"
import utils from "utils"

const GQL_GET_SAVED_SEARCHES = gql`
  query {
    savedSearches: mySearches {
      uuid
      name
      objectType
      query
    }
  }
`

const GQL_DELETE_SAVED_SEARCH = gql`
  mutation($uuid: String!) {
    deleteSavedSearch(uuid: $uuid)
  }
`

const MySavedSearches = ({ setSearchQuery, pageDispatchers }) => {
  const history = useHistory()
  const [stateError, setStateError] = useState(null)
  const [selectedSearch, setSelectedSearch] = useState(null)
  const { loading, error, data, refetch } = API.useApiQuery(
    GQL_GET_SAVED_SEARCHES
  )
  const { done, result } = useBoilerplate({
    loading,
    error,
    pageDispatchers
  })
  if (done) {
    return result
  }

  let savedSearches = []
  if (data) {
    savedSearches = data.savedSearches
    if (_isEmpty(savedSearches)) {
      if (selectedSearch) {
        // Clear selection
        setSelectedSearch(null)
      }
    } else if (!savedSearches.includes(selectedSearch)) {
      // Select first one
      setSelectedSearch(savedSearches[0])
    }
  }

  return (
    <Fieldset title="Saved searches">
      <Messages error={stateError} />
      <FormGroup controlId="savedSearchSelect">
        <ControlLabel>Select a saved search</ControlLabel>
        <FormControl componentClass="select" onChange={onSaveSearchSelect}>
          {savedSearches &&
            savedSearches.map(savedSearch => (
              <option value={savedSearch.uuid} key={savedSearch.uuid}>
                {savedSearch.name}
              </option>
            ))}
        </FormControl>
      </FormGroup>

      {selectedSearch && (

            <Button style={{ marginRight: 12 }} onClick={showSearch}>
              Show Search
            </Button>
            <ConfirmDestructive
              onConfirm={onConfirmDelete}
              objectType="search"
              objectDisplay={selectedSearch.name}
              bsStyle="danger"
              buttonLabel="Delete Search"
            />

          <SavedSearchTable search={selectedSearch} />

      )}
    </Fieldset>
  )

  function onSaveSearchSelect(event) {
    const uuid = event && event.target ? event.target.value : event
    const search = savedSearches.find(el => el.uuid === uuid)
    setSelectedSearch(search)
  }

  function showSearch() {
    if (selectedSearch) {
      const objType = SEARCH_OBJECT_TYPES[selectedSearch.objectType]
      const queryParams = utils.parseJsonSafe(selectedSearch.query)
      deserializeQueryParams(objType, queryParams, deserializeCallback)
    }
  }

  function deserializeCallback(objectType, filters, text) {
    // We update the Redux state
    setSearchQuery({
      objectType: objectType,
      filters: filters,
      text: text
    })
    history.push("/search")
  }

  function onConfirmDelete() {
    return API.mutation(GQL_DELETE_SAVED_SEARCH, { uuid: selectedSearch.uuid })
      .then(data => {
        refetch()
      })
      .catch(error => {
        setStateError(error)
        jumpToTop()
      })
  }
}

MySavedSearches.propTypes = {
  setSearchQuery: PropTypes.func.isRequired,
  pageDispatchers: PageDispatchersPropType
}

const mapDispatchToProps = (dispatch, ownProps) => {
  const pageDispatchers = mapPageDispatchersToProps(dispatch, ownProps)
  return {
    setSearchQuery: searchQuery => dispatch(setSearchQuery(searchQuery)),
    ...pageDispatchers
  }
}

export default connect(null, mapDispatchToProps)(MySavedSearches)
<commit_msg>NCI-Agency/anet#3698: Update MySavedSearched to Bootstrap 5 after merge<commit_after>import { gql } from "@apollo/client"
import { SEARCH_OBJECT_TYPES, setSearchQuery } from "actions"
import API from "api"
import ConfirmDestructive from "components/ConfirmDestructive"
import Fieldset from "components/Fieldset"
import Messages from "components/Messages"
import {
  jumpToTop,
  mapPageDispatchersToProps,
  PageDispatchersPropType,
  useBoilerplate
} from "components/Page"
import SavedSearchTable from "components/SavedSearchTable"
import { deserializeQueryParams } from "components/SearchFilters"
import _isEmpty from "lodash/isEmpty"
import PropTypes from "prop-types"
import React, { useState } from "react"
import { Button, Col, Form, Row } from "react-bootstrap"
import { connect } from "react-redux"
import { useHistory } from "react-router-dom"
import utils from "utils"

const GQL_GET_SAVED_SEARCHES = gql`
  query {
    savedSearches: mySearches {
      uuid
      name
      objectType
      query
    }
  }
`

const GQL_DELETE_SAVED_SEARCH = gql`
  mutation($uuid: String!) {
    deleteSavedSearch(uuid: $uuid)
  }
`

const MySavedSearches = ({ setSearchQuery, pageDispatchers }) => {
  const history = useHistory()
  const [stateError, setStateError] = useState(null)
  const [selectedSearch, setSelectedSearch] = useState(null)
  const { loading, error, data, refetch } = API.useApiQuery(
    GQL_GET_SAVED_SEARCHES
  )
  const { done, result } = useBoilerplate({
    loading,
    error,
    pageDispatchers
  })
  if (done) {
    return result
  }

  let savedSearches = []
  if (data) {
    savedSearches = data.savedSearches
    if (_isEmpty(savedSearches)) {
      if (selectedSearch) {
        // Clear selection
        setSelectedSearch(null)
      }
    } else if (!savedSearches.includes(selectedSearch)) {
      // Select first one
      setSelectedSearch(savedSearches[0])
    }
  }

  return (
    <Fieldset title="Saved searches">
      <Messages error={stateError} />
      <Form.Group as={Row} className="mb-3" controlId="savedSearchSelect">
        <Form.Label column sm={2}>
          Select a saved search
        </Form.Label>
        <Col sm={10}>
          <Form.Select onChange={onSaveSearchSelect}>
            {savedSearches &&
              savedSearches.map(savedSearch => (
                <option value={savedSearch.uuid} key={savedSearch.uuid}>
                  {savedSearch.name}
                </option>
              ))}
          </Form.Select>
        </Col>
      </Form.Group>

      {selectedSearch && (

          <Row>
            <Col sm={8}>
              <SavedSearchTable search={selectedSearch} />
            </Col>
            <Col className="text-end">
              <Button style={{ marginRight: 12 }} onClick={showSearch}>
                Show Search
              </Button>
              <ConfirmDestructive
                onConfirm={onConfirmDelete}
                objectType="search"
                objectDisplay={selectedSearch.name}
                variant="danger"
                buttonLabel="Delete Search"
              />
            </Col>
          </Row>

      )}
    </Fieldset>
  )

  function onSaveSearchSelect(event) {
    const uuid = event && event.target ? event.target.value : event
    const search = savedSearches.find(el => el.uuid === uuid)
    setSelectedSearch(search)
  }

  function showSearch() {
    if (selectedSearch) {
      const objType = SEARCH_OBJECT_TYPES[selectedSearch.objectType]
      const queryParams = utils.parseJsonSafe(selectedSearch.query)
      deserializeQueryParams(objType, queryParams, deserializeCallback)
    }
  }

  function deserializeCallback(objectType, filters, text) {
    // We update the Redux state
    setSearchQuery({
      objectType: objectType,
      filters: filters,
      text: text
    })
    history.push("/search")
  }

  function onConfirmDelete() {
    return API.mutation(GQL_DELETE_SAVED_SEARCH, { uuid: selectedSearch.uuid })
      .then(data => {
        refetch()
      })
      .catch(error => {
        setStateError(error)
        jumpToTop()
      })
  }
}

MySavedSearches.propTypes = {
  setSearchQuery: PropTypes.func.isRequired,
  pageDispatchers: PageDispatchersPropType
}

const mapDispatchToProps = (dispatch, ownProps) => {
  const pageDispatchers = mapPageDispatchersToProps(dispatch, ownProps)
  return {
    setSearchQuery: searchQuery => dispatch(setSearchQuery(searchQuery)),
    ...pageDispatchers
  }
}

export default connect(null, mapDispatchToProps)(MySavedSearches)
<|endoftext|>