/*!
 * @nuxt/server v2.15.8 (c) 2016-2021
 * Released under the MIT License
 * Repository: https://github.com/nuxt/nuxt.js
 * Website: https://nuxtjs.org
*/
'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

const path = require('path');
const consola = require('consola');
const launchMiddleware = require('launch-editor-middleware');
const serveStatic = require('serve-static');
const servePlaceholder = require('serve-placeholder');
const connect = require('connect');
const compression = require('compression');
const utils = require('@nuxt/utils');
const vueRenderer = require('@nuxt/vue-renderer');
const generateETag = require('etag');
const fresh = require('fresh');
const ufo = require('ufo');
const fs = require('fs-extra');
const Youch = require('@nuxtjs/youch');
const http = require('http');
const https = require('https');
const enableDestroy = require('server-destroy');
const ip = require('ip');
const pify = require('pify');
const onHeaders = require('on-headers');

function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }

function _interopNamespace(e) {
  if (e && e.__esModule) return e;
  var n = Object.create(null);
  if (e) {
    Object.keys(e).forEach(function (k) {
      if (k !== 'default') {
        var d = Object.getOwnPropertyDescriptor(e, k);
        Object.defineProperty(n, k, d.get ? d : {
          enumerable: true,
          get: function () {
            return e[k];
          }
        });
      }
    });
  }
  n['default'] = e;
  return Object.freeze(n);
}

const path__default = /*#__PURE__*/_interopDefaultLegacy(path);
const consola__default = /*#__PURE__*/_interopDefaultLegacy(consola);
const launchMiddleware__default = /*#__PURE__*/_interopDefaultLegacy(launchMiddleware);
const serveStatic__default = /*#__PURE__*/_interopDefaultLegacy(serveStatic);
const servePlaceholder__default = /*#__PURE__*/_interopDefaultLegacy(servePlaceholder);
const connect__default = /*#__PURE__*/_interopDefaultLegacy(connect);
const compression__default = /*#__PURE__*/_interopDefaultLegacy(compression);
const generateETag__default = /*#__PURE__*/_interopDefaultLegacy(generateETag);
const fresh__default = /*#__PURE__*/_interopDefaultLegacy(fresh);
const fs__default = /*#__PURE__*/_interopDefaultLegacy(fs);
const Youch__default = /*#__PURE__*/_interopDefaultLegacy(Youch);
const http__default = /*#__PURE__*/_interopDefaultLegacy(http);
const https__default = /*#__PURE__*/_interopDefaultLegacy(https);
const enableDestroy__default = /*#__PURE__*/_interopDefaultLegacy(enableDestroy);
const ip__default = /*#__PURE__*/_interopDefaultLegacy(ip);
const pify__default = /*#__PURE__*/_interopDefaultLegacy(pify);
const onHeaders__default = /*#__PURE__*/_interopDefaultLegacy(onHeaders);

class ServerContext {
  constructor (server) {
    this.nuxt = server.nuxt;
    this.globals = server.globals;
    this.options = server.options;
    this.resources = server.resources;
  }
}

async function renderAndGetWindow (
  url = 'http://localhost:3000',
  jsdomOpts = {},
  {
    loadedCallback,
    loadingTimeout = 2000,
    globals
  } = {}
) {
  const jsdom = await Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require('jsdom')); })
    .then(m => m.default || m)
    .catch((e) => {
      consola__default['default'].error(`
         jsdom is not installed. Please install jsdom with:
          $ yarn add --dev jsdom
          OR
          $ npm install --dev jsdom
        `);
      throw e
    });

  const options = Object.assign({
    // Load subresources (https://github.com/tmpvar/jsdom#loading-subresources)
    resources: 'usable',
    runScripts: 'dangerously',
    virtualConsole: true,
    beforeParse (window) {
      // Mock window.scrollTo
      window.scrollTo = () => {};
    }
  }, jsdomOpts);

  const jsdomErrHandler = (err) => {
    throw err
  };

  if (options.virtualConsole) {
    if (options.virtualConsole === true) {
      options.virtualConsole = new jsdom.VirtualConsole().sendTo(consola__default['default']);
    }
    // Throw error when window creation failed
    options.virtualConsole.on('jsdomError', jsdomErrHandler);
  } else {
    // If we get the virtualConsole option as `false` we should delete for don't pass it to `jsdom.JSDOM.fromURL`
    delete options.virtualConsole;
  }

  const { window } = await jsdom.JSDOM.fromURL(url, options);

  // If Nuxt could not be loaded (error from the server-side)
  const nuxtExists = window.document.body.innerHTML.includes(`id="${globals.id}"`);

  if (!nuxtExists) {
    const error = new Error('Could not load the nuxt app');
    error.body = window.document.body.innerHTML;
    window.close();
    throw error
  }

  // Used by Nuxt to say when the components are loaded and the app ready
  await utils.timeout(new Promise((resolve) => {
    window[loadedCallback] = () => resolve(window);
  }), loadingTimeout, `Components loading in renderAndGetWindow was not completed in ${loadingTimeout / 1000}s`);

  if (options.virtualConsole) {
    // After window initialized successfully
    options.virtualConsole.removeListener('jsdomError', jsdomErrHandler);
  }

  // Send back window object
  return window
}

const nuxtMiddleware = ({ options, nuxt, renderRoute, resources }) => async function nuxtMiddleware (req, res, next) {
  // Get context
  const context = utils.getContext(req, res);

  try {
    const url = ufo.normalizeURL(req.url);
    res.statusCode = 200;
    const result = await renderRoute(url, context);

    // If result is falsy, call renderLoading
    if (!result) {
      await nuxt.callHook('server:nuxt:renderLoading', req, res);
      return
    }

    await nuxt.callHook('render:route', url, result, context);
    const {
      html,
      cspScriptSrcHashes,
      error,
      redirected,
      preloadFiles
    } = result;

    if (redirected && context.target !== utils.TARGETS.static) {
      await nuxt.callHook('render:routeDone', url, result, context);
      return html
    }
    if (error) {
      res.statusCode = context.nuxt.error.statusCode || 500;
    }

    if (options.render.csp && cspScriptSrcHashes) {
      const { allowedSources, policies } = options.render.csp;
      const isReportOnly = !!options.render.csp.reportOnly;
      const cspHeader = isReportOnly ? 'Content-Security-Policy-Report-Only' : 'Content-Security-Policy';

      res.setHeader(cspHeader, getCspString({ cspScriptSrcHashes, allowedSources, policies, isReportOnly }));
    }

    // Add ETag header
    if (!error && options.render.etag) {
      const { hash } = options.render.etag;
      const etag = hash ? hash(html, options.render.etag) : generateETag__default['default'](html, options.render.etag);
      if (fresh__default['default'](req.headers, { etag })) {
        res.statusCode = 304;
        await nuxt.callHook('render:beforeResponse', url, result, context);
        res.end();
        await nuxt.callHook('render:routeDone', url, result, context);
        return
      }
      res.setHeader('ETag', etag);
    }

    // HTTP2 push headers for preload assets
    if (!error && options.render.http2.push) {
      // Parse resourceHints to extract HTTP.2 prefetch/push headers
      // https://w3c.github.io/preload/#server-push-http-2
      const { shouldPush, pushAssets } = options.render.http2;
      const { publicPath } = resources.clientManifest;

      const links = pushAssets
        ? pushAssets(req, res, publicPath, preloadFiles)
        : defaultPushAssets(preloadFiles, shouldPush, publicPath, options);

      // Pass with single Link header
      // https://blog.cloudflare.com/http-2-server-push-with-multiple-assets-per-link-header
      // https://www.w3.org/Protocols/9707-link-header.html
      if (links.length > 0) {
        res.setHeader('Link', links.join(', '));
      }
    }

    // Send response
    res.setHeader('Content-Type', 'text/html; charset=utf-8');
    res.setHeader('Accept-Ranges', 'none'); // #3870
    res.setHeader('Content-Length', Buffer.byteLength(html));
    await nuxt.callHook('render:beforeResponse', url, result, context);
    res.end(html, 'utf8');
    await nuxt.callHook('render:routeDone', url, result, context);
    return html
  } catch (err) {
    if (context && context.redirected) {
      consola__default['default'].error(err);
      return err
    }

    if (err.name === 'URIError') {
      err.statusCode = 400;
    }
    next(err);
  }
};

const defaultPushAssets = (preloadFiles, shouldPush, publicPath, options) => {
  if (shouldPush && options.dev) {
    consola__default['default'].warn('http2.shouldPush is deprecated. Use http2.pushAssets function');
  }

  const links = [];
  preloadFiles.forEach(({ file, asType, fileWithoutQuery, modern }) => {
    // By default, we only preload scripts or css
    if (!shouldPush && asType !== 'script' && asType !== 'style') {
      return
    }

    // User wants to explicitly control what to preload
    if (shouldPush && !shouldPush(fileWithoutQuery, asType)) {
      return
    }

    const { crossorigin } = options.render;
    const cors = `${crossorigin ? ` crossorigin=${crossorigin};` : ''}`;
    // `modulepreload` rel attribute only supports script-like `as` value
    // https://html.spec.whatwg.org/multipage/links.html#link-type-modulepreload
    const rel = modern && asType === 'script' ? 'modulepreload' : 'preload';

    links.push(`<${publicPath}${file}>; rel=${rel};${cors} as=${asType}`);
  });
  return links
};

const getCspString = ({ cspScriptSrcHashes, allowedSources, policies, isReportOnly }) => {
  const joinedHashes = cspScriptSrcHashes.join(' ');
  const baseCspStr = `script-src 'self' ${joinedHashes}`;
  const policyObjectAvailable = typeof policies === 'object' && policies !== null && !Array.isArray(policies);

  if (Array.isArray(allowedSources) && allowedSources.length) {
    return isReportOnly && policyObjectAvailable && !!policies['report-uri'] ? `${baseCspStr} ${allowedSources.join(' ')}; report-uri ${policies['report-uri']};` : `${baseCspStr} ${allowedSources.join(' ')}`
  }

  if (policyObjectAvailable) {
    const transformedPolicyObject = transformPolicyObject(policies, cspScriptSrcHashes);
    return Object.entries(transformedPolicyObject).map(([k, v]) => `${k} ${Array.isArray(v) ? v.join(' ') : v}`).join('; ')
  }

  return baseCspStr
};

const transformPolicyObject = (policies, cspScriptSrcHashes) => {
  const userHasDefinedScriptSrc = policies['script-src'] && Array.isArray(policies['script-src']);

  const additionalPolicies = userHasDefinedScriptSrc ? policies['script-src'] : [];

  // Self is always needed for inline-scripts, so add it, no matter if the user specified script-src himself.
  const hashAndPolicyList = cspScriptSrcHashes.concat('\'self\'', additionalPolicies);

  return { ...policies, 'script-src': hashAndPolicyList }
};

const errorMiddleware = ({ resources, options }) => async function errorMiddleware (_error, req, res, next) {
  // Normalize error
  const error = normalizeError(_error, options);

  const sendResponse = (content, type = 'text/html') => {
    // Set Headers
    res.statusCode = error.statusCode;
    res.statusMessage = 'RuntimeError';
    res.setHeader('Content-Type', type + '; charset=utf-8');
    res.setHeader('Content-Length', Buffer.byteLength(content));
    res.setHeader('Cache-Control', 'no-cache, no-store, max-age=0, must-revalidate');

    // Error headers
    if (error.headers) {
      for (const name in error.headers) {
        res.setHeader(name, error.headers[name]);
      }
    }

    // Send Response
    res.end(content, 'utf-8');
  };

  // Check if request accepts JSON
  const hasReqHeader = (header, includes) =>
    req.headers[header] && req.headers[header].toLowerCase().includes(includes);
  const isJson =
    hasReqHeader('accept', 'application/json') ||
    hasReqHeader('user-agent', 'curl/');

  // Use basic errors when debug mode is disabled
  if (!options.debug) {
    // We hide actual errors from end users, so show them on server logs
    if (error.statusCode !== 404) {
      consola__default['default'].error(error);
    }

    // Json format is compatible with Youch json responses
    const json = {
      status: error.statusCode,
      message: error.message,
      name: error.name
    };
    if (isJson) {
      sendResponse(JSON.stringify(json, undefined, 2), 'text/json');
      return
    }
    const html = resources.errorTemplate(json);
    sendResponse(html);
    return
  }

  // Show stack trace
  const youch = new Youch__default['default'](
    error,
    req,
    readSource,
    options.router.base,
    true
  );
  if (isJson) {
    const json = await youch.toJSON();
    sendResponse(JSON.stringify(json, undefined, 2), 'text/json');
    return
  }

  const html = await youch.toHTML();
  sendResponse(html);
};

const sanitizeName = name => name ? name.replace('webpack:///', '').split('?')[0] : null;

const normalizeError = (_error, { srcDir, rootDir, buildDir }) => {
  if (typeof _error === 'string') {
    _error = { message: _error };
  } else if (!_error) {
    _error = { message: '<empty>' };
  }

  const error = new Error(_error.message);
  error.name = _error.name;
  error.statusCode = _error.statusCode || 500;
  error.headers = _error.headers;

  const searchPath = [
    srcDir,
    rootDir,
    path__default['default'].join(buildDir, 'dist', 'server'),
    buildDir,
    process.cwd()
  ];

  const findInPaths = (fileName) => {
    for (const dir of searchPath) {
      const fullPath = path__default['default'].resolve(dir, fileName);
      if (fs__default['default'].existsSync(fullPath)) {
        return fullPath
      }
    }
    return fileName
  };

  error.stack = (_error.stack || '')
    .split('\n')
    .map((line) => {
      const match = line.match(/\(([^)]+)\)|([^\s]+\.[^\s]+):/);
      if (!match) {
        return line
      }
      const src = match[1] || match[2] || '';
      return line.replace(src, findInPaths(sanitizeName(src)))
    })
    .join('\n');

  return error
};

async function readSource (frame) {
  if (fs__default['default'].existsSync(frame.fileName)) {
    frame.fullPath = frame.fileName; // Youch BW compat
    frame.contents = await fs__default['default'].readFile(frame.fileName, 'utf-8');
  }
}

let RANDOM_PORT = '0';

class Listener {
  constructor ({ port, host, socket, https, app, dev, baseURL }) {
    // Options
    this.port = port;
    this.host = host;
    this.socket = socket;
    this.https = https;
    this.app = app;
    this.dev = dev;
    this.baseURL = baseURL;

    // After listen
    this.listening = false;
    this._server = null;
    this.server = null;
    this.address = null;
    this.url = null;
  }

  async close () {
    // Destroy server by forcing every connection to be closed
    if (this.server && this.server.listening) {
      await this.server.destroy();
      consola__default['default'].debug('server closed');
    }

    // Delete references
    this.listening = false;
    this._server = null;
    this.server = null;
    this.address = null;
    this.url = null;
  }

  computeURL () {
    const address = this.server.address();
    if (!this.socket) {
      switch (address.address) {
        case '127.0.0.1': this.host = 'localhost'; break
        case '0.0.0.0': this.host = ip__default['default'].address(); break
      }
      this.port = address.port;
      this.url = `http${this.https ? 's' : ''}://${this.host}:${this.port}${this.baseURL}`;
      this.url = decodeURI(this.url);
      return
    }
    this.url = `unix+http://${address}`;
  }

  async listen () {
    // Prevent multi calls
    if (this.listening) {
      return
    }

    // Initialize underlying http(s) server
    const protocol = this.https ? https__default['default'] : http__default['default'];
    const protocolOpts = this.https ? [this.https] : [];
    this._server = protocol.createServer.apply(protocol, protocolOpts.concat(this.app));

    // Call server.listen
    // Prepare listenArgs
    const listenArgs = this.socket ? { path: this.socket } : { host: this.host, port: this.port };
    listenArgs.exclusive = false;

    // Call server.listen
    try {
      this.server = await new Promise((resolve, reject) => {
        this._server.on('error', error => reject(error));
        const s = this._server.listen(listenArgs, error => error ? reject(error) : resolve(s));
      });
    } catch (error) {
      return this.serverErrorHandler(error)
    }

    // Enable destroy support
    enableDestroy__default['default'](this.server);
    pify__default['default'](this.server.destroy);

    // Compute listen URL
    this.computeURL();

    // Set this.listening to true
    this.listening = true;
  }

  async serverErrorHandler (error) {
    // Detect if port is not available
    const addressInUse = error.code === 'EADDRINUSE';

    // Use better error message
    if (addressInUse) {
      const address = this.socket || `${this.host}:${this.port}`;
      error.message = `Address \`${address}\` is already in use.`;

      // Listen to a random port on dev as a fallback
      if (this.dev && !this.socket && this.port !== RANDOM_PORT) {
        consola__default['default'].warn(error.message);
        consola__default['default'].info('Trying a random port...');
        this.port = RANDOM_PORT;
        await this.close();
        await this.listen();
        RANDOM_PORT = this.port;
        return
      }
    }

    // Throw error
    throw error
  }
}

const createTimingMiddleware = options => (req, res, next) => {
  if (res.timing) {
    consola__default['default'].warn('server-timing is already registered.');
  }
  res.timing = new ServerTiming();

  if (options && options.total) {
    res.timing.start('total', 'Nuxt Server Time');
  }

  onHeaders__default['default'](res, () => {
    res.timing.end('total');

    if (res.timing.headers.length > 0) {
      res.setHeader(
        'Server-Timing',
        []
          .concat(res.getHeader('Server-Timing') || [])
          .concat(res.timing.headers)
          .join(', ')
      );
    }
    res.timing.clear();
  });

  next();
};

class ServerTiming extends utils.Timer {
  constructor (...args) {
    super(...args);
    this.headers = [];
  }

  end (...args) {
    const time = super.end(...args);
    if (time) {
      this.headers.push(this.formatHeader(time));
    }
    return time
  }

  clear () {
    super.clear();
    this.headers.length = 0;
  }

  formatHeader (time) {
    const desc = time.description ? `;desc="${time.description}"` : '';
    return `${time.name};dur=${time.duration}${desc}`
  }
}

class Server {
  constructor (nuxt) {
    this.nuxt = nuxt;
    this.options = nuxt.options;

    this.globals = utils.determineGlobals(nuxt.options.globalName, nuxt.options.globals);

    this.publicPath = utils.isUrl(this.options.build.publicPath)
      ? this.options.build._publicPath
      : this.options.build.publicPath.replace(/^\.+\//, '/');

    // Runtime shared resources
    this.resources = {};

    // Will be set after listen
    this.listeners = [];

    // Create new connect instance
    this.app = connect__default['default']();

    // Close hook
    this.nuxt.hook('close', () => this.close());

    // devMiddleware placeholder
    if (this.options.dev) {
      this.nuxt.hook('server:devMiddleware', (devMiddleware) => {
        this.devMiddleware = devMiddleware;
      });
    }
  }

  async ready () {
    if (this._readyCalled) {
      return this
    }
    this._readyCalled = true;

    await this.nuxt.callHook('render:before', this, this.options.render);

    // Initialize vue-renderer
    this.serverContext = new ServerContext(this);
    this.renderer = new vueRenderer.VueRenderer(this.serverContext);
    await this.renderer.ready();

    // Setup nuxt middleware
    await this.setupMiddleware();

    // Call done hook
    await this.nuxt.callHook('render:done', this);

    return this
  }

  async setupMiddleware () {
    // Apply setupMiddleware from modules first
    await this.nuxt.callHook('render:setupMiddleware', this.app);

    // Compression middleware for production
    if (!this.options.dev) {
      const { compressor } = this.options.render;
      if (typeof compressor === 'object') {
        // If only setting for `compression` are provided, require the module and insert
        this.useMiddleware(compression__default['default'](compressor));
      } else if (compressor) {
        // Else, require own compression middleware if compressor is actually truthy
        this.useMiddleware(compressor);
      }
    }

    if (this.options.server.timing) {
      this.useMiddleware(createTimingMiddleware(this.options.server.timing));
    }

    // For serving static/ files to /
    const staticMiddleware = serveStatic__default['default'](
      path__default['default'].resolve(this.options.srcDir, this.options.dir.static),
      this.options.render.static
    );
    staticMiddleware.prefix = this.options.render.static.prefix;
    this.useMiddleware(staticMiddleware);

    // Serve .nuxt/dist/client files only for production
    // For dev they will be served with devMiddleware
    if (!this.options.dev) {
      const distDir = path__default['default'].resolve(this.options.buildDir, 'dist', 'client');
      this.useMiddleware({
        path: this.publicPath,
        handler: serveStatic__default['default'](
          distDir,
          this.options.render.dist
        )
      });
    }

    // Dev middleware
    if (this.options.dev) {
      this.useMiddleware((req, res, next) => {
        if (!this.devMiddleware) {
          return next()
        }
        // Safari over-caches JS (breaking HMR) and the seemingly only way to turn
        // this off in dev mode is to set Vary: * header
        // #3828, #9034
        if (req.url.startsWith(this.publicPath) && req.url.endsWith('.js')) {
          res.setHeader('Vary', '*');
        }
        this.devMiddleware(req, res, next);
      });

      // open in editor for debug mode only
      if (this.options.debug) {
        this.useMiddleware({
          path: '__open-in-editor',
          handler: launchMiddleware__default['default'](this.options.editor)
        });
      }
    }

    // Add user provided middleware
    for (const m of this.options.serverMiddleware) {
      this.useMiddleware(m);
    }

    // Graceful 404 error handler
    const { fallback } = this.options.render;
    if (fallback) {
      // Dist files
      if (fallback.dist) {
        this.useMiddleware({
          path: this.publicPath,
          handler: servePlaceholder__default['default'](fallback.dist)
        });
      }

      // Other paths
      if (fallback.static) {
        this.useMiddleware({
          path: '/',
          handler: servePlaceholder__default['default'](fallback.static)
        });
      }
    }

    // Finally use nuxtMiddleware
    this.useMiddleware(nuxtMiddleware({
      options: this.options,
      nuxt: this.nuxt,
      renderRoute: this.renderRoute.bind(this),
      resources: this.resources
    }));

    // DX: redirect if router.base in development
    const routerBase = this.nuxt.options.router.base;
    if (this.options.dev && routerBase !== '/') {
      this.useMiddleware({
        prefix: false,
        handler: (req, res, next) => {
          if (decodeURI(req.url).startsWith(decodeURI(routerBase))) {
            return next()
          }
          const to = utils.urlJoin(routerBase, req.url);
          consola__default['default'].info(`[Development] Redirecting from \`${decodeURI(req.url)}\` to \`${decodeURI(to)}\` (router.base specified)`);
          res.writeHead(302, {
            Location: to
          });
          res.end();
        }
      });
    }

    // Apply errorMiddleware from modules first
    await this.nuxt.callHook('render:errorMiddleware', this.app);

    // Error middleware for errors that occurred in middleware that declared above
    this.useMiddleware(errorMiddleware({
      resources: this.resources,
      options: this.options
    }));
  }

  _normalizeMiddleware (middleware) {
    // Normalize plain function
    if (typeof middleware === 'function') {
      middleware = { handle: middleware };
    }

    // If a plain string provided as path to middleware
    if (typeof middleware === 'string') {
      middleware = this._requireMiddleware(middleware);
    }

    // #8584
    // shallow clone the middleware before any change is made,
    // in case any following mutation breaks when applied repeatedly.
    middleware = Object.assign({}, middleware);

    // Normalize handler to handle (backward compatibility)
    if (middleware.handler && !middleware.handle) {
      middleware.handle = middleware.handler;
      delete middleware.handler;
    }

    // Normalize path to route (backward compatibility)
    if (middleware.path && !middleware.route) {
      middleware.route = middleware.path;
      delete middleware.path;
    }

    // If handle is a string pointing to path
    if (typeof middleware.handle === 'string') {
      Object.assign(middleware, this._requireMiddleware(middleware.handle));
    }

    // No handle
    if (!middleware.handle) {
      middleware.handle = (req, res, next) => {
        next(new Error('ServerMiddleware should expose a handle: ' + middleware.entry));
      };
    }

    // Prefix on handle (proxy-module)
    if (middleware.handle.prefix !== undefined && middleware.prefix === undefined) {
      middleware.prefix = middleware.handle.prefix;
    }

    // sub-app (express)
    if (typeof middleware.handle.handle === 'function') {
      const server = middleware.handle;
      middleware.handle = server.handle.bind(server);
    }

    return middleware
  }

  _requireMiddleware (entry) {
    // Resolve entry
    entry = this.nuxt.resolver.resolvePath(entry);

    // Require middleware
    let middleware;
    try {
      middleware = this.nuxt.resolver.requireModule(entry);
    } catch (error) {
      // Show full error
      consola__default['default'].error('ServerMiddleware Error:', error);

      // Placeholder for error
      middleware = (req, res, next) => { next(error); };
    }

    // Normalize
    middleware = this._normalizeMiddleware(middleware);

    // Set entry
    middleware.entry = entry;

    return middleware
  }

  resolveMiddleware (middleware, fallbackRoute = '/') {
    // Ensure middleware is normalized
    middleware = this._normalizeMiddleware(middleware);

    // Fallback route
    if (!middleware.route) {
      middleware.route = fallbackRoute;
    }

    // #8584
    // save the original route before applying defaults
    middleware._originalRoute = middleware.route;

    // Resolve final route
    middleware.route = (
      (middleware.prefix !== false ? this.options.router.base : '') +
      (typeof middleware.route === 'string' ? middleware.route : '')
    ).replace(/\/\//g, '/');

    // Strip trailing slash
    if (middleware.route.endsWith('/')) {
      middleware.route = middleware.route.slice(0, -1);
    }

    // Assign _middleware to handle to make accessible from app.stack
    middleware.handle._middleware = middleware;

    return middleware
  }

  useMiddleware (middleware) {
    const { route, handle } = this.resolveMiddleware(middleware);
    this.app.use(route, handle);
  }

  replaceMiddleware (query, middleware) {
    let serverStackItem;

    if (typeof query === 'string') {
      // Search by entry
      serverStackItem = this.app.stack.find(({ handle }) => handle._middleware && handle._middleware.entry === query);
    } else {
      // Search by reference
      serverStackItem = this.app.stack.find(({ handle }) => handle === query);
    }

    // Stop if item not found
    if (!serverStackItem) {
      return
    }

    // unload middleware
    this.unloadMiddleware(serverStackItem);

    // Resolve middleware
    const { route, handle } = this.resolveMiddleware(
      middleware,
      // #8584 pass the original route as fallback
      serverStackItem.handle._middleware
        ? serverStackItem.handle._middleware._originalRoute
        : serverStackItem.route
    );

    // Update serverStackItem
    serverStackItem.handle = handle;

    // Error State
    serverStackItem.route = route;

    // Return updated item
    return serverStackItem
  }

  unloadMiddleware ({ handle }) {
    if (handle._middleware && typeof handle._middleware.unload === 'function') {
      handle._middleware.unload();
    }
  }

  serverMiddlewarePaths () {
    return this.app.stack.map(({ handle }) => handle._middleware && handle._middleware.entry).filter(Boolean)
  }

  renderRoute () {
    return this.renderer.renderRoute.apply(this.renderer, arguments)
  }

  loadResources () {
    return this.renderer.loadResources.apply(this.renderer, arguments)
  }

  renderAndGetWindow (url, opts = {}, {
    loadingTimeout = 2000,
    loadedCallback = this.globals.loadedCallback,
    globals = this.globals
  } = {}) {
    return renderAndGetWindow(url, opts, {
      loadingTimeout,
      loadedCallback,
      globals
    })
  }

  async listen (port, host, socket) {
    // Ensure nuxt is ready
    await this.nuxt.ready();

    // Create a new listener
    const listener = new Listener({
      port: isNaN(parseInt(port)) ? this.options.server.port : port,
      host: host || this.options.server.host,
      socket: socket || this.options.server.socket,
      https: this.options.server.https,
      app: this.app,
      dev: this.options.dev,
      baseURL: this.options.router.base
    });

    // Listen
    await listener.listen();

    // Push listener to this.listeners
    this.listeners.push(listener);

    await this.nuxt.callHook('listen', listener.server, listener);

    return listener
  }

  async close () {
    if (this.__closed) {
      return
    }
    this.__closed = true;

    await Promise.all(this.listeners.map(l => l.close()));

    this.listeners = [];

    if (typeof this.renderer.close === 'function') {
      await this.renderer.close();
    }

    this.app.stack.forEach(this.unloadMiddleware);
    this.app.removeAllListeners();
    this.app = null;

    for (const key in this.resources) {
      delete this.resources[key];
    }
  }
}

exports.Listener = Listener;
exports.Server = Server;
