Exit Full View

Games Cupboard / build / js / node_modules / webpack-dev-server / lib / utils / normalizeOptions.js

'use strict';

const os = require('os');
const path = require('path');
const del = require('del');
const fs = require('graceful-fs');
const getCompilerConfigArray = require('./getCompilerConfigArray');

function normalizeOptions(compiler, options, logger, cacheDir) {
  // TODO: improve this to not use .find for compiler watchOptions
  const configArray = getCompilerConfigArray(compiler);
  const watchOptionsConfig = configArray.find(
    (config) => config.watch !== false && config.watchOptions
  );
  const watchOptions = watchOptionsConfig
    ? watchOptionsConfig.watchOptions
    : {};

  const defaultOptionsForStatic = {
    directory: path.join(process.cwd(), 'public'),
    staticOptions: {},
    publicPath: ['/'],
    serveIndex: { icons: true },
    // Respect options from compiler watchOptions
    watch: watchOptions,
  };

  if (typeof options.allowedHosts === 'undefined') {
    // allowedHosts allows some default hosts picked from
    // `options.host` or `webSocketURL.hostname` and `localhost`
    options.allowedHosts = 'auto';
  }
  if (
    typeof options.allowedHosts === 'string' &&
    options.allowedHosts !== 'auto' &&
    options.allowedHosts !== 'all'
  ) {
    // we store allowedHosts as array when supplied as string
    options.allowedHosts = [options.allowedHosts];
  }

  if (
    typeof options.client === 'undefined' ||
    (typeof options.client === 'object' && options.client !== null)
  ) {
    if (!options.client) {
      options.client = {};
    }

    if (typeof options.client.webSocketURL === 'undefined') {
      options.client.webSocketURL = {};
    } else if (typeof options.client.webSocketURL === 'string') {
      const parsedURL = new URL(options.client.webSocketURL);

      options.client.webSocketURL = {
        protocol: parsedURL.protocol,
        hostname: parsedURL.hostname,
        port: parsedURL.port.length > 0 ? Number(parsedURL.port) : '',
        pathname: parsedURL.pathname,
        username: parsedURL.username,
        password: parsedURL.password,
      };
    } else if (typeof options.client.webSocketURL.port === 'string') {
      options.client.webSocketURL.port = Number(
        options.client.webSocketURL.port
      );
    }

    // Enable client overlay by default
    if (typeof options.client.overlay === 'undefined') {
      options.client.overlay = true;
    } else if (typeof options.client.overlay !== 'boolean') {
      options.client.overlay = {
        errors: true,
        warnings: true,
        ...options.client.overlay,
      };
    }
  }

  if (typeof options.compress === 'undefined') {
    options.compress = true;
  }

  options.devMiddleware = options.devMiddleware || {};

  options.hot =
    typeof options.hot === 'boolean' || options.hot === 'only'
      ? options.hot
      : true;

  // if the user enables http2, we can safely enable https
  if ((options.http2 && !options.https) || options.https === true) {
    options.https = {
      requestCert: false,
    };
  }

  // https option
  if (options.https) {
    for (const property of ['cacert', 'pfx', 'key', 'cert']) {
      const value = options.https[property];
      const isBuffer = value instanceof Buffer;

      if (value && !isBuffer) {
        let stats = null;

        try {
          stats = fs.lstatSync(fs.realpathSync(value)).isFile();
        } catch (error) {
          // ignore error
        }

        // It is file
        options.https[property] = stats
          ? fs.readFileSync(path.resolve(value))
          : value;
      }
    }

    let fakeCert;

    if (!options.https.key || !options.https.cert) {
      const certificateDir = cacheDir || os.tmpdir();
      const certificatePath = path.join(certificateDir, 'server.pem');
      let certificateExists = fs.existsSync(certificatePath);

      if (certificateExists) {
        const certificateTtl = 1000 * 60 * 60 * 24;
        const certificateStat = fs.statSync(certificatePath);

        const now = new Date();

        // cert is more than 30 days old, kill it with fire
        if ((now - certificateStat.ctime) / certificateTtl > 30) {
          logger.info('SSL Certificate is more than 30 days old. Removing.');

          del.sync([certificatePath], { force: true });

          certificateExists = false;
        }
      }

      if (!certificateExists) {
        logger.info('Generating SSL Certificate');

        const selfsigned = require('selfsigned');
        const attributes = [{ name: 'commonName', value: 'localhost' }];
        const pems = selfsigned.generate(attributes, {
          algorithm: 'sha256',
          days: 30,
          keySize: 2048,
          extensions: [
            // {
            //   name: 'basicConstraints',
            //   cA: true,
            // },
            {
              name: 'keyUsage',
              keyCertSign: true,
              digitalSignature: true,
              nonRepudiation: true,
              keyEncipherment: true,
              dataEncipherment: true,
            },
            {
              name: 'extKeyUsage',
              serverAuth: true,
              clientAuth: true,
              codeSigning: true,
              timeStamping: true,
            },
            {
              name: 'subjectAltName',
              altNames: [
                {
                  // type 2 is DNS
                  type: 2,
                  value: 'localhost',
                },
                {
                  type: 2,
                  value: 'localhost.localdomain',
                },
                {
                  type: 2,
                  value: 'lvh.me',
                },
                {
                  type: 2,
                  value: '*.lvh.me',
                },
                {
                  type: 2,
                  value: '[::1]',
                },
                {
                  // type 7 is IP
                  type: 7,
                  ip: '127.0.0.1',
                },
                {
                  type: 7,
                  ip: 'fe80::1',
                },
              ],
            },
          ],
        });

        fs.mkdirSync(certificateDir, { recursive: true });
        fs.writeFileSync(certificatePath, pems.private + pems.cert, {
          encoding: 'utf8',
        });
      }

      fakeCert = fs.readFileSync(certificatePath);
    }

    options.https.key = options.https.key || fakeCert;
    options.https.cert = options.https.cert || fakeCert;
  }

  if (typeof options.ipc === 'boolean') {
    const isWindows = process.platform === 'win32';
    const pipePrefix = isWindows ? '\\\\.\\pipe\\' : os.tmpdir();
    const pipeName = 'webpack-dev-server.sock';

    options.ipc = path.join(pipePrefix, pipeName);
  }

  options.liveReload =
    typeof options.liveReload !== 'undefined' ? options.liveReload : true;

  // https://github.com/webpack/webpack-dev-server/issues/1990
  const defaultOpenOptions = { wait: false };
  const getOpenItemsFromObject = ({ target, ...rest }) => {
    const normalizedOptions = { ...defaultOpenOptions, ...rest };

    if (typeof normalizedOptions.app === 'string') {
      normalizedOptions.app = {
        name: normalizedOptions.app,
      };
    }

    const normalizedTarget = typeof target === 'undefined' ? '<url>' : target;

    if (Array.isArray(normalizedTarget)) {
      return normalizedTarget.map((singleTarget) => {
        return { target: singleTarget, options: normalizedOptions };
      });
    }

    return [{ target: normalizedTarget, options: normalizedOptions }];
  };

  if (typeof options.open === 'undefined') {
    options.open = [];
  } else if (typeof options.open === 'boolean') {
    options.open = options.open
      ? [{ target: '<url>', options: defaultOpenOptions }]
      : [];
  } else if (typeof options.open === 'string') {
    options.open = [{ target: options.open, options: defaultOpenOptions }];
  } else if (Array.isArray(options.open)) {
    const result = [];

    options.open.forEach((item) => {
      if (typeof item === 'string') {
        result.push({ target: item, options: defaultOpenOptions });

        return;
      }

      result.push(...getOpenItemsFromObject(item));
    });

    options.open = result;
  } else {
    options.open = [...getOpenItemsFromObject(options.open)];
  }

  if (typeof options.port === 'string' && options.port !== 'auto') {
    options.port = Number(options.port);
  }

  /**
   * Assume a proxy configuration specified as:
   * proxy: {
   *   'context': { options }
   * }
   * OR
   * proxy: {
   *   'context': 'target'
   * }
   */
  if (typeof options.proxy !== 'undefined') {
    if (!Array.isArray(options.proxy)) {
      if (Object.prototype.hasOwnProperty.call(options.proxy, 'target')) {
        options.proxy = [options.proxy];
      } else {
        options.proxy = Object.keys(options.proxy).map((context) => {
          let proxyOptions;
          // For backwards compatibility reasons.
          const correctedContext = context
            .replace(/^\*$/, '**')
            .replace(/\/\*$/, '');

          if (typeof options.proxy[context] === 'string') {
            proxyOptions = {
              context: correctedContext,
              target: options.proxy[context],
            };
          } else {
            proxyOptions = { ...options.proxy[context] };
            proxyOptions.context = correctedContext;
          }

          const getLogLevelForProxy = (level) => {
            if (level === 'none') {
              return 'silent';
            }

            if (level === 'log') {
              return 'info';
            }

            if (level === 'verbose') {
              return 'debug';
            }

            return level;
          };

          const configs = getCompilerConfigArray(compiler);
          const configWithDevServer =
            configs.find((config) => config.devServer) || configs[0];

          if (typeof proxyOptions.logLevel === 'undefined') {
            proxyOptions.logLevel = getLogLevelForProxy(
              configWithDevServer.infrastructureLogging.level
            );
          }

          if (typeof proxyOptions.logProvider === 'undefined') {
            proxyOptions.logProvider = () => logger;
          }

          return proxyOptions;
        });
      }
    }
  }

  if (typeof options.setupExitSignals === 'undefined') {
    options.setupExitSignals = true;
  }

  if (typeof options.static === 'undefined') {
    options.static = [defaultOptionsForStatic];
  } else if (typeof options.static === 'boolean') {
    options.static = options.static ? [defaultOptionsForStatic] : false;
  } else if (typeof options.static === 'string') {
    options.static = [
      { ...defaultOptionsForStatic, directory: options.static },
    ];
  } else if (Array.isArray(options.static)) {
    options.static = options.static.map((item) => {
      if (typeof item === 'string') {
        return { ...defaultOptionsForStatic, directory: item };
      }

      return { ...defaultOptionsForStatic, ...item };
    });
  } else {
    options.static = [{ ...defaultOptionsForStatic, ...options.static }];
  }

  if (options.static) {
    const isAbsoluteUrl = require('is-absolute-url');

    options.static.forEach((staticOption) => {
      if (isAbsoluteUrl(staticOption.directory)) {
        throw new Error('Using a URL as static.directory is not supported');
      }

      // ensure that publicPath is an array
      if (typeof staticOption.publicPath === 'string') {
        staticOption.publicPath = [staticOption.publicPath];
      }

      // ensure that watch is an object if true
      if (staticOption.watch === true) {
        staticOption.watch = defaultOptionsForStatic.watch;
      }

      // ensure that serveIndex is an object if true
      if (staticOption.serveIndex === true) {
        staticOption.serveIndex = defaultOptionsForStatic.serveIndex;
      }
    });
  }

  const defaultWebSocketServerType = 'ws';
  const defaultWebSocketServerOptions = { path: '/ws' };

  if (typeof options.webSocketServer === 'undefined') {
    options.webSocketServer = {
      type: defaultWebSocketServerType,
      options: defaultWebSocketServerOptions,
    };
  } else if (
    typeof options.webSocketServer === 'boolean' &&
    !options.webSocketServer
  ) {
    options.webSocketServer = false;
  } else if (
    typeof options.webSocketServer === 'string' ||
    typeof options.webSocketServer === 'function'
  ) {
    options.webSocketServer = {
      type: options.webSocketServer,
      options: defaultWebSocketServerOptions,
    };
  } else {
    options.webSocketServer = {
      type: options.webSocketServer.type || defaultWebSocketServerType,
      options: {
        ...defaultWebSocketServerOptions,
        ...options.webSocketServer.options,
      },
    };

    if (typeof options.webSocketServer.options.port === 'string') {
      options.webSocketServer.options.port = Number(
        options.webSocketServer.options.port
      );
    }
  }
}

module.exports = normalizeOptions;