/*! * @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: '' }; } 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;