@sitecore-jss/sitecore-jss-proxy
Advanced tools
Comparing version 22.2.0-canary.37 to 22.2.0-canary.38
"use strict"; | ||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } | ||
return new (P || (P = Promise))(function (resolve, reject) { | ||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } | ||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } | ||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } | ||
step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||
}); | ||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { | ||
if (k2 === undefined) k2 = k; | ||
var desc = Object.getOwnPropertyDescriptor(m, k); | ||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { | ||
desc = { enumerable: true, get: function() { return m[k]; } }; | ||
} | ||
Object.defineProperty(o, k2, desc); | ||
}) : (function(o, m, k, k2) { | ||
if (k2 === undefined) k2 = k; | ||
o[k2] = m[k]; | ||
})); | ||
var __exportStar = (this && this.__exportStar) || function(m, exports) { | ||
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); | ||
}; | ||
var __importDefault = (this && this.__importDefault) || function (mod) { | ||
return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.rewriteRequestPath = exports.removeEmptyAnalyticsCookie = void 0; | ||
const http_proxy_middleware_1 = require("http-proxy-middleware"); | ||
const http_status_codes_1 = __importDefault(require("http-status-codes")); | ||
const set_cookie_parser_1 = __importDefault(require("set-cookie-parser")); | ||
const zlib_1 = __importDefault(require("zlib")); // node.js standard lib | ||
const util_1 = require("./util"); | ||
// For some reason, every other response returned by Sitecore contains the 'set-cookie' header with the SC_ANALYTICS_GLOBAL_COOKIE value as an empty string. | ||
// This effectively sets the cookie to empty on the client as well, so if a user were to close their browser | ||
// after one of these 'empty value' responses, they would not be tracked as a returning visitor after re-opening their browser. | ||
// To address this, we simply parse the response cookies and if the analytics cookie is present but has an empty value, then we | ||
// remove it from the response header. This means the existing cookie in the browser remains intact. | ||
const removeEmptyAnalyticsCookie = (proxyResponse) => { | ||
const cookies = set_cookie_parser_1.default.parse(proxyResponse.headers['set-cookie']); | ||
if (cookies) { | ||
const analyticsCookieIndex = cookies.findIndex((c) => c.name === 'SC_ANALYTICS_GLOBAL_COOKIE'); | ||
if (analyticsCookieIndex !== -1) { | ||
const analyticsCookie = cookies[analyticsCookieIndex]; | ||
if (analyticsCookie && analyticsCookie.value === '') { | ||
cookies.splice(analyticsCookieIndex, 1); | ||
/* eslint-disable no-param-reassign */ | ||
proxyResponse.headers['set-cookie'] = cookies; | ||
/* eslint-enable no-param-reassign */ | ||
} | ||
} | ||
} | ||
}; | ||
exports.removeEmptyAnalyticsCookie = removeEmptyAnalyticsCookie; | ||
// inspired by: http://stackoverflow.com/a/22487927/9324 | ||
/** | ||
* @param {IncomingMessage} proxyResponse | ||
* @param {IncomingMessage} request | ||
* @param {ServerResponse} serverResponse | ||
* @param {AppRenderer} renderer | ||
* @param {ProxyConfig} config | ||
*/ | ||
function renderAppToResponse(proxyResponse, request, serverResponse, renderer, config) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
// monkey-patch FTW? | ||
const originalWriteHead = serverResponse.writeHead; | ||
const originalWrite = serverResponse.write; | ||
const originalEnd = serverResponse.end; | ||
// these lines are necessary and must happen before we do any writing to the response | ||
// otherwise the headers will have already been sent | ||
delete proxyResponse.headers['content-length']; | ||
proxyResponse.headers['content-type'] = 'text/html; charset=utf-8'; | ||
// remove IIS server header for security | ||
delete proxyResponse.headers.server; | ||
if (config.setHeaders) { | ||
config.setHeaders(request, serverResponse, proxyResponse); | ||
} | ||
const contentEncoding = proxyResponse.headers['content-encoding']; | ||
if (contentEncoding && | ||
(contentEncoding.indexOf('gzip') !== -1 || contentEncoding.indexOf('deflate') !== -1)) { | ||
delete proxyResponse.headers['content-encoding']; | ||
} | ||
// we are going to set our own status code if rendering fails | ||
serverResponse.writeHead = () => serverResponse; | ||
// buffer the response body as it is written for later processing | ||
let buf = Buffer.from(''); | ||
serverResponse.write = (data, encoding) => { | ||
if (Buffer.isBuffer(data)) { | ||
buf = Buffer.concat([buf, data]); // append raw buffer | ||
} | ||
else { | ||
buf = Buffer.concat([buf, Buffer.from(data, encoding)]); // append string with optional character encoding (default utf8) | ||
} | ||
// sanity check: if the response is huge, bail. | ||
// ...we don't want to let someone bring down the server by filling up all our RAM. | ||
if (buf.length > config.maxResponseSizeBytes) { | ||
throw new Error('Document too large'); | ||
} | ||
return true; | ||
}; | ||
/** | ||
* Extract layout service data from proxy response | ||
*/ | ||
function extractLayoutServiceDataFromProxyResponse() { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
if (proxyResponse.statusCode === http_status_codes_1.default.OK || | ||
proxyResponse.statusCode === http_status_codes_1.default.NOT_FOUND) { | ||
let responseString; | ||
if (contentEncoding && | ||
(contentEncoding.indexOf('gzip') !== -1 || contentEncoding.indexOf('deflate') !== -1)) { | ||
responseString = new Promise((resolve, reject) => { | ||
if (config.debug) { | ||
console.log('Layout service response is compressed; decompressing.'); | ||
} | ||
zlib_1.default.unzip(buf, (error, result) => { | ||
if (error) { | ||
reject(error); | ||
} | ||
if (result) { | ||
resolve(result.toString('utf-8')); | ||
} | ||
}); | ||
}); | ||
} | ||
else { | ||
responseString = Promise.resolve(buf.toString('utf-8')); | ||
} | ||
return responseString.then(util_1.tryParseJson); | ||
} | ||
return Promise.resolve(null); | ||
}); | ||
} | ||
/** | ||
* function replies with HTTP 500 when an error occurs | ||
* @param {Error} error | ||
*/ | ||
function replyWithError(error) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
console.error(error); | ||
let errorResponse = { | ||
statusCode: proxyResponse.statusCode || http_status_codes_1.default.INTERNAL_SERVER_ERROR, | ||
content: proxyResponse.statusMessage || 'Internal Server Error', | ||
headers: {}, | ||
}; | ||
if (config.onError) { | ||
const onError = yield config.onError(error, proxyResponse); | ||
errorResponse = Object.assign(Object.assign({}, errorResponse), onError); | ||
} | ||
completeProxyResponse(Buffer.from(errorResponse.content), errorResponse.statusCode, errorResponse.headers); | ||
}); | ||
} | ||
// callback handles the result of server-side rendering | ||
/** | ||
* @param {Error | null} error | ||
* @param {RenderResponse} result | ||
*/ | ||
function handleRenderingResult(error, result) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
if (!error && !result) { | ||
return replyWithError(new Error('Render function did not return a result or an error!')); | ||
} | ||
if (error) { | ||
return replyWithError(error); | ||
} | ||
if (!result) { | ||
// should not occur, but makes TS happy | ||
return replyWithError(new Error('Render function result did not return a result.')); | ||
} | ||
if (!result.html) { | ||
return replyWithError(new Error('Render function result was returned but html property was falsy.')); | ||
} | ||
if (config.transformSSRContent) { | ||
result.html = yield config.transformSSRContent(result, request, serverResponse); | ||
} | ||
// we have to convert back to a buffer so that we can get the *byte count* (rather than character count) of the body | ||
let content = Buffer.from(result.html); | ||
// setting the content-length header is not absolutely necessary, but is recommended | ||
proxyResponse.headers['content-length'] = content.length.toString(10); | ||
// if original request was a HEAD, we should not return a response body | ||
if (request.method === 'HEAD') { | ||
if (config.debug) { | ||
console.log('DEBUG: Original request method was HEAD, clearing response body'); | ||
} | ||
content = Buffer.from([]); | ||
} | ||
if (result.redirect) { | ||
if (!result.status) { | ||
result.status = 302; | ||
} | ||
proxyResponse.headers.location = result.redirect; | ||
} | ||
const finalStatusCode = result.status || proxyResponse.statusCode || http_status_codes_1.default.OK; | ||
if (config.debug) { | ||
console.log('DEBUG: FINAL response headers for client', JSON.stringify(proxyResponse.headers, null, 2)); | ||
console.log('DEBUG: FINAL status code for client', finalStatusCode); | ||
} | ||
completeProxyResponse(content, finalStatusCode); | ||
}); | ||
} | ||
/** | ||
* @param {Buffer | null} content | ||
* @param {number} statusCode | ||
* @param {IncomingHttpHeaders} [headers] | ||
*/ | ||
function completeProxyResponse(content, statusCode, headers) { | ||
if (!headers) { | ||
headers = proxyResponse.headers; | ||
} | ||
originalWriteHead.apply(serverResponse, [statusCode, headers]); | ||
if (content) | ||
originalWrite.call(serverResponse, content); | ||
originalEnd.call(serverResponse); | ||
} | ||
/** | ||
* @param {object} layoutServiceData | ||
*/ | ||
function createViewBag(layoutServiceData) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
let viewBag = { | ||
statusCode: proxyResponse.statusCode, | ||
dictionary: {}, | ||
}; | ||
if (config.createViewBag) { | ||
const customViewBag = yield config.createViewBag(request, serverResponse, proxyResponse, layoutServiceData); | ||
viewBag = Object.assign(Object.assign({}, viewBag), customViewBag); | ||
} | ||
return viewBag; | ||
}); | ||
} | ||
// as the response is ending, we parse the current response body which is JSON, then | ||
// render the app using that JSON, but return HTML to the final response. | ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||
// @ts-ignore | ||
serverResponse.end = () => __awaiter(this, void 0, void 0, function* () { | ||
try { | ||
const layoutServiceData = yield extractLayoutServiceDataFromProxyResponse(); | ||
const viewBag = yield createViewBag(layoutServiceData); | ||
if (!layoutServiceData) { | ||
throw new Error(`Received invalid response ${proxyResponse.statusCode} ${proxyResponse.statusMessage}`); | ||
} | ||
return renderer(handleRenderingResult, | ||
// originalUrl not defined in `http-proxy-middleware` types but it exists | ||
request.originalUrl, layoutServiceData, viewBag); | ||
} | ||
catch (error) { | ||
return replyWithError(error); | ||
} | ||
}); | ||
}); | ||
} | ||
/** | ||
* @param {IncomingMessage} proxyResponse | ||
* @param {Request} request | ||
* @param {Response} serverResponse | ||
* @param {AppRenderer} renderer | ||
* @param {ProxyConfig} config | ||
*/ | ||
function handleProxyResponse(proxyResponse, request, serverResponse, renderer, config) { | ||
(0, exports.removeEmptyAnalyticsCookie)(proxyResponse); | ||
if (config.debug) { | ||
console.log('DEBUG: request url', request.url); | ||
console.log('DEBUG: request query', request.query); | ||
console.log('DEBUG: request original url', request.originalUrl); | ||
console.log('DEBUG: proxied request response code', proxyResponse.statusCode); | ||
console.log('DEBUG: RAW request headers', JSON.stringify(request.headers, null, 2)); | ||
console.log('DEBUG: RAW headers from the proxied response', JSON.stringify(proxyResponse.headers, null, 2)); | ||
} | ||
// if the request URL contains any of the excluded rewrite routes, we assume the response does not need to be server rendered. | ||
// instead, the response should just be relayed as usual. | ||
if (isUrlIgnored(request.originalUrl, config, true)) { | ||
return Promise.resolve(undefined); | ||
} | ||
// your first thought might be: why do we need to render the app here? why not just pass the JSON response to another piece of middleware that will render the app? | ||
// the answer: the proxy middleware ends the response and does not "chain" | ||
return renderAppToResponse(proxyResponse, request, serverResponse, renderer, config); | ||
} | ||
/** | ||
* @param {string} reqPath | ||
* @param {Request} req | ||
* @param {ProxyConfig} config | ||
* @param {RouteUrlParser} parseRouteUrl | ||
*/ | ||
function rewriteRequestPath(reqPath, req, config, parseRouteUrl) { | ||
// the path comes in URL-encoded by default, | ||
// but we don't want that because... | ||
// 1. We need to URL-encode it before we send it out to the Layout Service, if it matches a route | ||
// 2. We don't want to force people to URL-encode ignored routes, etc (just use spaces instead of %20, etc) | ||
const decodedReqPath = decodeURIComponent(reqPath); | ||
// if the request URL contains a path/route that should not be re-written, then just pass it along as-is | ||
if (isUrlIgnored(reqPath, config)) { | ||
// we do not return the decoded URL because we're using it verbatim - should be encoded. | ||
return reqPath; | ||
} | ||
// if the request URL doesn't contain the layout service controller path, assume we need to rewrite the request URL so that it does | ||
// if this seems redundant, it is. the config.pathRewriteExcludeRoutes should contain the layout service path, but can't always assume that it will... | ||
if (decodedReqPath.indexOf(config.layoutServiceRoute) !== -1) { | ||
return reqPath; | ||
} | ||
let finalReqPath = decodedReqPath; | ||
const qsIndex = finalReqPath.indexOf('?'); | ||
let qs = ''; | ||
if (qsIndex > -1 || Object.keys(req.query).length) { | ||
qs = (0, util_1.buildQueryString)(req.query); | ||
// Splice qs part when url contains that | ||
if (qsIndex > -1) | ||
finalReqPath = finalReqPath.slice(0, qsIndex); | ||
} | ||
if (config.qsParams) { | ||
if (qs) { | ||
qs += '&'; | ||
} | ||
qs += `${config.qsParams}`; | ||
} | ||
let lang; | ||
if (parseRouteUrl) { | ||
if (config.debug) { | ||
console.log(`DEBUG: Parsing route URL using ${decodedReqPath} URL...`); | ||
} | ||
const routeParams = parseRouteUrl(finalReqPath); | ||
if (routeParams) { | ||
if (routeParams.sitecoreRoute) { | ||
finalReqPath = routeParams.sitecoreRoute; | ||
} | ||
else { | ||
finalReqPath = '/'; | ||
} | ||
if (!finalReqPath.startsWith('/')) { | ||
finalReqPath = `/${finalReqPath}`; | ||
} | ||
lang = routeParams.lang; | ||
if (routeParams.qsParams) { | ||
qs += `&${routeParams.qsParams}`; | ||
} | ||
if (config.debug) { | ||
console.log('DEBUG: parseRouteUrl() result', routeParams); | ||
} | ||
} | ||
} | ||
let path = `${config.layoutServiceRoute}?item=${encodeURIComponent(finalReqPath)}&sc_apikey=${config.apiKey}`; | ||
if (lang) { | ||
path = `${path}&sc_lang=${lang}`; | ||
} | ||
if (qs) { | ||
path = `${path}&${qs}`; | ||
} | ||
return path; | ||
} | ||
exports.rewriteRequestPath = rewriteRequestPath; | ||
/** | ||
* @param {string} originalUrl | ||
* @param {ProxyConfig} config | ||
* @param {boolean} noDebug | ||
*/ | ||
function isUrlIgnored(originalUrl, config, noDebug = false) { | ||
if (config.pathRewriteExcludePredicate && config.pathRewriteExcludeRoutes) { | ||
console.error('ERROR: pathRewriteExcludePredicate and pathRewriteExcludeRoutes were both provided in config. Provide only one.'); | ||
process.exit(1); | ||
} | ||
let result = null; | ||
if (config.pathRewriteExcludeRoutes) { | ||
const matchRoute = decodeURIComponent(originalUrl).toUpperCase(); | ||
result = config.pathRewriteExcludeRoutes.find((excludedRoute) => excludedRoute.length > 0 && matchRoute.startsWith(excludedRoute)); | ||
if (!noDebug && config.debug) { | ||
if (!result) { | ||
console.log(`DEBUG: URL ${originalUrl} did not match the proxy exclude list, and will be treated as a layout service route to render. Excludes:`, config.pathRewriteExcludeRoutes); | ||
} | ||
else { | ||
console.log(`DEBUG: URL ${originalUrl} matched the proxy exclude list and will be served verbatim as received. Excludes: `, config.pathRewriteExcludeRoutes); | ||
} | ||
} | ||
return result ? true : false; | ||
} | ||
if (config.pathRewriteExcludePredicate) { | ||
result = config.pathRewriteExcludePredicate(originalUrl); | ||
if (!noDebug && config.debug) { | ||
if (!result) { | ||
console.log(`DEBUG: URL ${originalUrl} did not match the proxy exclude function, and will be treated as a layout service route to render.`); | ||
} | ||
else { | ||
console.log(`DEBUG: URL ${originalUrl} matched the proxy exclude function and will be served verbatim as received.`); | ||
} | ||
} | ||
return result; | ||
} | ||
return false; | ||
} | ||
/** | ||
* @param {ClientRequest} proxyReq | ||
* @param {Request} req | ||
* @param {Response} res | ||
* @param {ServerOptions} options | ||
* @param {ProxyConfig} config | ||
* @param {Function} customOnProxyReq | ||
*/ | ||
function handleProxyRequest(proxyReq, req, res, options, config, customOnProxyReq) { | ||
if (!isUrlIgnored(req.originalUrl, config, true)) { | ||
// In case 'followRedirects' is enabled, and before the proxy was initialized we had set 'originalMethod' | ||
// now we need to set req.method back to original one, since proxyReq is already initialized. | ||
// See more info in 'preProxyHandler' | ||
if (options.followRedirects && req.originalMethod === 'HEAD') { | ||
req.method = req.originalMethod; | ||
delete req.originalMethod; | ||
if (config.debug) { | ||
console.log('DEBUG: Rewriting HEAD request to GET to create accurate headers'); | ||
} | ||
} | ||
else if (proxyReq.method === 'HEAD') { | ||
if (config.debug) { | ||
console.log('DEBUG: Rewriting HEAD request to GET to create accurate headers'); | ||
} | ||
// if a HEAD request, we still need to issue a GET so we can return accurate headers | ||
proxyReq.method = 'GET'; | ||
} | ||
} | ||
// invoke custom onProxyReq | ||
if (customOnProxyReq) { | ||
customOnProxyReq(proxyReq, req, res, options); | ||
} | ||
} | ||
/** | ||
* @param {AppRenderer} renderer | ||
* @param {ProxyConfig} config | ||
* @param {RouteUrlParser} parseRouteUrl | ||
*/ | ||
function createOptions(renderer, config, parseRouteUrl) { | ||
var _a; | ||
if (!config.maxResponseSizeBytes) { | ||
config.maxResponseSizeBytes = 10 * 1024 * 1024; | ||
} | ||
// ensure all excludes are case insensitive | ||
if (config.pathRewriteExcludeRoutes && Array.isArray(config.pathRewriteExcludeRoutes)) { | ||
config.pathRewriteExcludeRoutes = config.pathRewriteExcludeRoutes.map((exclude) => exclude.toUpperCase()); | ||
} | ||
if (config.debug) { | ||
console.log('DEBUG: Final proxy config', config); | ||
} | ||
const customOnProxyReq = (_a = config.proxyOptions) === null || _a === void 0 ? void 0 : _a.onProxyReq; | ||
return Object.assign(Object.assign({}, config.proxyOptions), { target: config.apiHost, changeOrigin: true, ws: config.ws || false, pathRewrite: (reqPath, req) => rewriteRequestPath(reqPath, req, config, parseRouteUrl), logLevel: config.debug ? 'debug' : 'info', onProxyReq: (proxyReq, req, res, options) => handleProxyRequest(proxyReq, req, res, options, config, customOnProxyReq), onProxyRes: (proxyRes, req, res) => handleProxyResponse(proxyRes, req, res, renderer, config) }); | ||
} | ||
/** | ||
* @param {AppRenderer} renderer | ||
* @param {ProxyConfig} config | ||
* @param {RouteUrlParser} parseRouteUrl | ||
*/ | ||
function scProxy(renderer, config, parseRouteUrl) { | ||
const options = createOptions(renderer, config, parseRouteUrl); | ||
const preProxyHandler = (req, _res, next) => { | ||
// When 'followRedirects' is enabled, 'onProxyReq' is executed after 'proxyReq' is initialized based on original 'req' | ||
// and there are no public properties/methods to modify Redirectable 'proxyReq'. | ||
// so, we need to set 'HEAD' req as 'GET' before the proxy is initialized. | ||
// During the 'onProxyReq' event we will set 'req.method' back as 'HEAD'. | ||
// if a HEAD request, we need to issue a GET so we can return accurate headers | ||
if (req.method === 'HEAD' && | ||
options.followRedirects && | ||
!isUrlIgnored(req.originalUrl, config, true)) { | ||
req.method = 'GET'; | ||
req.originalMethod = 'HEAD'; | ||
} | ||
next(); | ||
}; | ||
return [preProxyHandler, (0, http_proxy_middleware_1.createProxyMiddleware)(options)]; | ||
} | ||
exports.default = scProxy; | ||
__exportStar(require("./middleware"), exports); | ||
__exportStar(require("./types"), exports); |
@@ -1,441 +0,2 @@ | ||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } | ||
return new (P || (P = Promise))(function (resolve, reject) { | ||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } | ||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } | ||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } | ||
step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||
}); | ||
}; | ||
import { createProxyMiddleware } from 'http-proxy-middleware'; | ||
import HttpStatus from 'http-status-codes'; | ||
import setCookieParser from 'set-cookie-parser'; | ||
import zlib from 'zlib'; // node.js standard lib | ||
import { buildQueryString, tryParseJson } from './util'; | ||
// For some reason, every other response returned by Sitecore contains the 'set-cookie' header with the SC_ANALYTICS_GLOBAL_COOKIE value as an empty string. | ||
// This effectively sets the cookie to empty on the client as well, so if a user were to close their browser | ||
// after one of these 'empty value' responses, they would not be tracked as a returning visitor after re-opening their browser. | ||
// To address this, we simply parse the response cookies and if the analytics cookie is present but has an empty value, then we | ||
// remove it from the response header. This means the existing cookie in the browser remains intact. | ||
export const removeEmptyAnalyticsCookie = (proxyResponse) => { | ||
const cookies = setCookieParser.parse(proxyResponse.headers['set-cookie']); | ||
if (cookies) { | ||
const analyticsCookieIndex = cookies.findIndex((c) => c.name === 'SC_ANALYTICS_GLOBAL_COOKIE'); | ||
if (analyticsCookieIndex !== -1) { | ||
const analyticsCookie = cookies[analyticsCookieIndex]; | ||
if (analyticsCookie && analyticsCookie.value === '') { | ||
cookies.splice(analyticsCookieIndex, 1); | ||
/* eslint-disable no-param-reassign */ | ||
proxyResponse.headers['set-cookie'] = cookies; | ||
/* eslint-enable no-param-reassign */ | ||
} | ||
} | ||
} | ||
}; | ||
// inspired by: http://stackoverflow.com/a/22487927/9324 | ||
/** | ||
* @param {IncomingMessage} proxyResponse | ||
* @param {IncomingMessage} request | ||
* @param {ServerResponse} serverResponse | ||
* @param {AppRenderer} renderer | ||
* @param {ProxyConfig} config | ||
*/ | ||
function renderAppToResponse(proxyResponse, request, serverResponse, renderer, config) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
// monkey-patch FTW? | ||
const originalWriteHead = serverResponse.writeHead; | ||
const originalWrite = serverResponse.write; | ||
const originalEnd = serverResponse.end; | ||
// these lines are necessary and must happen before we do any writing to the response | ||
// otherwise the headers will have already been sent | ||
delete proxyResponse.headers['content-length']; | ||
proxyResponse.headers['content-type'] = 'text/html; charset=utf-8'; | ||
// remove IIS server header for security | ||
delete proxyResponse.headers.server; | ||
if (config.setHeaders) { | ||
config.setHeaders(request, serverResponse, proxyResponse); | ||
} | ||
const contentEncoding = proxyResponse.headers['content-encoding']; | ||
if (contentEncoding && | ||
(contentEncoding.indexOf('gzip') !== -1 || contentEncoding.indexOf('deflate') !== -1)) { | ||
delete proxyResponse.headers['content-encoding']; | ||
} | ||
// we are going to set our own status code if rendering fails | ||
serverResponse.writeHead = () => serverResponse; | ||
// buffer the response body as it is written for later processing | ||
let buf = Buffer.from(''); | ||
serverResponse.write = (data, encoding) => { | ||
if (Buffer.isBuffer(data)) { | ||
buf = Buffer.concat([buf, data]); // append raw buffer | ||
} | ||
else { | ||
buf = Buffer.concat([buf, Buffer.from(data, encoding)]); // append string with optional character encoding (default utf8) | ||
} | ||
// sanity check: if the response is huge, bail. | ||
// ...we don't want to let someone bring down the server by filling up all our RAM. | ||
if (buf.length > config.maxResponseSizeBytes) { | ||
throw new Error('Document too large'); | ||
} | ||
return true; | ||
}; | ||
/** | ||
* Extract layout service data from proxy response | ||
*/ | ||
function extractLayoutServiceDataFromProxyResponse() { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
if (proxyResponse.statusCode === HttpStatus.OK || | ||
proxyResponse.statusCode === HttpStatus.NOT_FOUND) { | ||
let responseString; | ||
if (contentEncoding && | ||
(contentEncoding.indexOf('gzip') !== -1 || contentEncoding.indexOf('deflate') !== -1)) { | ||
responseString = new Promise((resolve, reject) => { | ||
if (config.debug) { | ||
console.log('Layout service response is compressed; decompressing.'); | ||
} | ||
zlib.unzip(buf, (error, result) => { | ||
if (error) { | ||
reject(error); | ||
} | ||
if (result) { | ||
resolve(result.toString('utf-8')); | ||
} | ||
}); | ||
}); | ||
} | ||
else { | ||
responseString = Promise.resolve(buf.toString('utf-8')); | ||
} | ||
return responseString.then(tryParseJson); | ||
} | ||
return Promise.resolve(null); | ||
}); | ||
} | ||
/** | ||
* function replies with HTTP 500 when an error occurs | ||
* @param {Error} error | ||
*/ | ||
function replyWithError(error) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
console.error(error); | ||
let errorResponse = { | ||
statusCode: proxyResponse.statusCode || HttpStatus.INTERNAL_SERVER_ERROR, | ||
content: proxyResponse.statusMessage || 'Internal Server Error', | ||
headers: {}, | ||
}; | ||
if (config.onError) { | ||
const onError = yield config.onError(error, proxyResponse); | ||
errorResponse = Object.assign(Object.assign({}, errorResponse), onError); | ||
} | ||
completeProxyResponse(Buffer.from(errorResponse.content), errorResponse.statusCode, errorResponse.headers); | ||
}); | ||
} | ||
// callback handles the result of server-side rendering | ||
/** | ||
* @param {Error | null} error | ||
* @param {RenderResponse} result | ||
*/ | ||
function handleRenderingResult(error, result) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
if (!error && !result) { | ||
return replyWithError(new Error('Render function did not return a result or an error!')); | ||
} | ||
if (error) { | ||
return replyWithError(error); | ||
} | ||
if (!result) { | ||
// should not occur, but makes TS happy | ||
return replyWithError(new Error('Render function result did not return a result.')); | ||
} | ||
if (!result.html) { | ||
return replyWithError(new Error('Render function result was returned but html property was falsy.')); | ||
} | ||
if (config.transformSSRContent) { | ||
result.html = yield config.transformSSRContent(result, request, serverResponse); | ||
} | ||
// we have to convert back to a buffer so that we can get the *byte count* (rather than character count) of the body | ||
let content = Buffer.from(result.html); | ||
// setting the content-length header is not absolutely necessary, but is recommended | ||
proxyResponse.headers['content-length'] = content.length.toString(10); | ||
// if original request was a HEAD, we should not return a response body | ||
if (request.method === 'HEAD') { | ||
if (config.debug) { | ||
console.log('DEBUG: Original request method was HEAD, clearing response body'); | ||
} | ||
content = Buffer.from([]); | ||
} | ||
if (result.redirect) { | ||
if (!result.status) { | ||
result.status = 302; | ||
} | ||
proxyResponse.headers.location = result.redirect; | ||
} | ||
const finalStatusCode = result.status || proxyResponse.statusCode || HttpStatus.OK; | ||
if (config.debug) { | ||
console.log('DEBUG: FINAL response headers for client', JSON.stringify(proxyResponse.headers, null, 2)); | ||
console.log('DEBUG: FINAL status code for client', finalStatusCode); | ||
} | ||
completeProxyResponse(content, finalStatusCode); | ||
}); | ||
} | ||
/** | ||
* @param {Buffer | null} content | ||
* @param {number} statusCode | ||
* @param {IncomingHttpHeaders} [headers] | ||
*/ | ||
function completeProxyResponse(content, statusCode, headers) { | ||
if (!headers) { | ||
headers = proxyResponse.headers; | ||
} | ||
originalWriteHead.apply(serverResponse, [statusCode, headers]); | ||
if (content) | ||
originalWrite.call(serverResponse, content); | ||
originalEnd.call(serverResponse); | ||
} | ||
/** | ||
* @param {object} layoutServiceData | ||
*/ | ||
function createViewBag(layoutServiceData) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
let viewBag = { | ||
statusCode: proxyResponse.statusCode, | ||
dictionary: {}, | ||
}; | ||
if (config.createViewBag) { | ||
const customViewBag = yield config.createViewBag(request, serverResponse, proxyResponse, layoutServiceData); | ||
viewBag = Object.assign(Object.assign({}, viewBag), customViewBag); | ||
} | ||
return viewBag; | ||
}); | ||
} | ||
// as the response is ending, we parse the current response body which is JSON, then | ||
// render the app using that JSON, but return HTML to the final response. | ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||
// @ts-ignore | ||
serverResponse.end = () => __awaiter(this, void 0, void 0, function* () { | ||
try { | ||
const layoutServiceData = yield extractLayoutServiceDataFromProxyResponse(); | ||
const viewBag = yield createViewBag(layoutServiceData); | ||
if (!layoutServiceData) { | ||
throw new Error(`Received invalid response ${proxyResponse.statusCode} ${proxyResponse.statusMessage}`); | ||
} | ||
return renderer(handleRenderingResult, | ||
// originalUrl not defined in `http-proxy-middleware` types but it exists | ||
request.originalUrl, layoutServiceData, viewBag); | ||
} | ||
catch (error) { | ||
return replyWithError(error); | ||
} | ||
}); | ||
}); | ||
} | ||
/** | ||
* @param {IncomingMessage} proxyResponse | ||
* @param {Request} request | ||
* @param {Response} serverResponse | ||
* @param {AppRenderer} renderer | ||
* @param {ProxyConfig} config | ||
*/ | ||
function handleProxyResponse(proxyResponse, request, serverResponse, renderer, config) { | ||
removeEmptyAnalyticsCookie(proxyResponse); | ||
if (config.debug) { | ||
console.log('DEBUG: request url', request.url); | ||
console.log('DEBUG: request query', request.query); | ||
console.log('DEBUG: request original url', request.originalUrl); | ||
console.log('DEBUG: proxied request response code', proxyResponse.statusCode); | ||
console.log('DEBUG: RAW request headers', JSON.stringify(request.headers, null, 2)); | ||
console.log('DEBUG: RAW headers from the proxied response', JSON.stringify(proxyResponse.headers, null, 2)); | ||
} | ||
// if the request URL contains any of the excluded rewrite routes, we assume the response does not need to be server rendered. | ||
// instead, the response should just be relayed as usual. | ||
if (isUrlIgnored(request.originalUrl, config, true)) { | ||
return Promise.resolve(undefined); | ||
} | ||
// your first thought might be: why do we need to render the app here? why not just pass the JSON response to another piece of middleware that will render the app? | ||
// the answer: the proxy middleware ends the response and does not "chain" | ||
return renderAppToResponse(proxyResponse, request, serverResponse, renderer, config); | ||
} | ||
/** | ||
* @param {string} reqPath | ||
* @param {Request} req | ||
* @param {ProxyConfig} config | ||
* @param {RouteUrlParser} parseRouteUrl | ||
*/ | ||
export function rewriteRequestPath(reqPath, req, config, parseRouteUrl) { | ||
// the path comes in URL-encoded by default, | ||
// but we don't want that because... | ||
// 1. We need to URL-encode it before we send it out to the Layout Service, if it matches a route | ||
// 2. We don't want to force people to URL-encode ignored routes, etc (just use spaces instead of %20, etc) | ||
const decodedReqPath = decodeURIComponent(reqPath); | ||
// if the request URL contains a path/route that should not be re-written, then just pass it along as-is | ||
if (isUrlIgnored(reqPath, config)) { | ||
// we do not return the decoded URL because we're using it verbatim - should be encoded. | ||
return reqPath; | ||
} | ||
// if the request URL doesn't contain the layout service controller path, assume we need to rewrite the request URL so that it does | ||
// if this seems redundant, it is. the config.pathRewriteExcludeRoutes should contain the layout service path, but can't always assume that it will... | ||
if (decodedReqPath.indexOf(config.layoutServiceRoute) !== -1) { | ||
return reqPath; | ||
} | ||
let finalReqPath = decodedReqPath; | ||
const qsIndex = finalReqPath.indexOf('?'); | ||
let qs = ''; | ||
if (qsIndex > -1 || Object.keys(req.query).length) { | ||
qs = buildQueryString(req.query); | ||
// Splice qs part when url contains that | ||
if (qsIndex > -1) | ||
finalReqPath = finalReqPath.slice(0, qsIndex); | ||
} | ||
if (config.qsParams) { | ||
if (qs) { | ||
qs += '&'; | ||
} | ||
qs += `${config.qsParams}`; | ||
} | ||
let lang; | ||
if (parseRouteUrl) { | ||
if (config.debug) { | ||
console.log(`DEBUG: Parsing route URL using ${decodedReqPath} URL...`); | ||
} | ||
const routeParams = parseRouteUrl(finalReqPath); | ||
if (routeParams) { | ||
if (routeParams.sitecoreRoute) { | ||
finalReqPath = routeParams.sitecoreRoute; | ||
} | ||
else { | ||
finalReqPath = '/'; | ||
} | ||
if (!finalReqPath.startsWith('/')) { | ||
finalReqPath = `/${finalReqPath}`; | ||
} | ||
lang = routeParams.lang; | ||
if (routeParams.qsParams) { | ||
qs += `&${routeParams.qsParams}`; | ||
} | ||
if (config.debug) { | ||
console.log('DEBUG: parseRouteUrl() result', routeParams); | ||
} | ||
} | ||
} | ||
let path = `${config.layoutServiceRoute}?item=${encodeURIComponent(finalReqPath)}&sc_apikey=${config.apiKey}`; | ||
if (lang) { | ||
path = `${path}&sc_lang=${lang}`; | ||
} | ||
if (qs) { | ||
path = `${path}&${qs}`; | ||
} | ||
return path; | ||
} | ||
/** | ||
* @param {string} originalUrl | ||
* @param {ProxyConfig} config | ||
* @param {boolean} noDebug | ||
*/ | ||
function isUrlIgnored(originalUrl, config, noDebug = false) { | ||
if (config.pathRewriteExcludePredicate && config.pathRewriteExcludeRoutes) { | ||
console.error('ERROR: pathRewriteExcludePredicate and pathRewriteExcludeRoutes were both provided in config. Provide only one.'); | ||
process.exit(1); | ||
} | ||
let result = null; | ||
if (config.pathRewriteExcludeRoutes) { | ||
const matchRoute = decodeURIComponent(originalUrl).toUpperCase(); | ||
result = config.pathRewriteExcludeRoutes.find((excludedRoute) => excludedRoute.length > 0 && matchRoute.startsWith(excludedRoute)); | ||
if (!noDebug && config.debug) { | ||
if (!result) { | ||
console.log(`DEBUG: URL ${originalUrl} did not match the proxy exclude list, and will be treated as a layout service route to render. Excludes:`, config.pathRewriteExcludeRoutes); | ||
} | ||
else { | ||
console.log(`DEBUG: URL ${originalUrl} matched the proxy exclude list and will be served verbatim as received. Excludes: `, config.pathRewriteExcludeRoutes); | ||
} | ||
} | ||
return result ? true : false; | ||
} | ||
if (config.pathRewriteExcludePredicate) { | ||
result = config.pathRewriteExcludePredicate(originalUrl); | ||
if (!noDebug && config.debug) { | ||
if (!result) { | ||
console.log(`DEBUG: URL ${originalUrl} did not match the proxy exclude function, and will be treated as a layout service route to render.`); | ||
} | ||
else { | ||
console.log(`DEBUG: URL ${originalUrl} matched the proxy exclude function and will be served verbatim as received.`); | ||
} | ||
} | ||
return result; | ||
} | ||
return false; | ||
} | ||
/** | ||
* @param {ClientRequest} proxyReq | ||
* @param {Request} req | ||
* @param {Response} res | ||
* @param {ServerOptions} options | ||
* @param {ProxyConfig} config | ||
* @param {Function} customOnProxyReq | ||
*/ | ||
function handleProxyRequest(proxyReq, req, res, options, config, customOnProxyReq) { | ||
if (!isUrlIgnored(req.originalUrl, config, true)) { | ||
// In case 'followRedirects' is enabled, and before the proxy was initialized we had set 'originalMethod' | ||
// now we need to set req.method back to original one, since proxyReq is already initialized. | ||
// See more info in 'preProxyHandler' | ||
if (options.followRedirects && req.originalMethod === 'HEAD') { | ||
req.method = req.originalMethod; | ||
delete req.originalMethod; | ||
if (config.debug) { | ||
console.log('DEBUG: Rewriting HEAD request to GET to create accurate headers'); | ||
} | ||
} | ||
else if (proxyReq.method === 'HEAD') { | ||
if (config.debug) { | ||
console.log('DEBUG: Rewriting HEAD request to GET to create accurate headers'); | ||
} | ||
// if a HEAD request, we still need to issue a GET so we can return accurate headers | ||
proxyReq.method = 'GET'; | ||
} | ||
} | ||
// invoke custom onProxyReq | ||
if (customOnProxyReq) { | ||
customOnProxyReq(proxyReq, req, res, options); | ||
} | ||
} | ||
/** | ||
* @param {AppRenderer} renderer | ||
* @param {ProxyConfig} config | ||
* @param {RouteUrlParser} parseRouteUrl | ||
*/ | ||
function createOptions(renderer, config, parseRouteUrl) { | ||
var _a; | ||
if (!config.maxResponseSizeBytes) { | ||
config.maxResponseSizeBytes = 10 * 1024 * 1024; | ||
} | ||
// ensure all excludes are case insensitive | ||
if (config.pathRewriteExcludeRoutes && Array.isArray(config.pathRewriteExcludeRoutes)) { | ||
config.pathRewriteExcludeRoutes = config.pathRewriteExcludeRoutes.map((exclude) => exclude.toUpperCase()); | ||
} | ||
if (config.debug) { | ||
console.log('DEBUG: Final proxy config', config); | ||
} | ||
const customOnProxyReq = (_a = config.proxyOptions) === null || _a === void 0 ? void 0 : _a.onProxyReq; | ||
return Object.assign(Object.assign({}, config.proxyOptions), { target: config.apiHost, changeOrigin: true, ws: config.ws || false, pathRewrite: (reqPath, req) => rewriteRequestPath(reqPath, req, config, parseRouteUrl), logLevel: config.debug ? 'debug' : 'info', onProxyReq: (proxyReq, req, res, options) => handleProxyRequest(proxyReq, req, res, options, config, customOnProxyReq), onProxyRes: (proxyRes, req, res) => handleProxyResponse(proxyRes, req, res, renderer, config) }); | ||
} | ||
/** | ||
* @param {AppRenderer} renderer | ||
* @param {ProxyConfig} config | ||
* @param {RouteUrlParser} parseRouteUrl | ||
*/ | ||
export default function scProxy(renderer, config, parseRouteUrl) { | ||
const options = createOptions(renderer, config, parseRouteUrl); | ||
const preProxyHandler = (req, _res, next) => { | ||
// When 'followRedirects' is enabled, 'onProxyReq' is executed after 'proxyReq' is initialized based on original 'req' | ||
// and there are no public properties/methods to modify Redirectable 'proxyReq'. | ||
// so, we need to set 'HEAD' req as 'GET' before the proxy is initialized. | ||
// During the 'onProxyReq' event we will set 'req.method' back as 'HEAD'. | ||
// if a HEAD request, we need to issue a GET so we can return accurate headers | ||
if (req.method === 'HEAD' && | ||
options.followRedirects && | ||
!isUrlIgnored(req.originalUrl, config, true)) { | ||
req.method = 'GET'; | ||
req.originalMethod = 'HEAD'; | ||
} | ||
next(); | ||
}; | ||
return [preProxyHandler, createProxyMiddleware(options)]; | ||
} | ||
export * from './middleware'; | ||
export * from './types'; |
{ | ||
"name": "@sitecore-jss/sitecore-jss-proxy", | ||
"version": "22.2.0-canary.37", | ||
"description": "Proxy middleware for express.js server.", | ||
"version": "22.2.0-canary.38", | ||
"description": "Middlewares, utilities to work in a headless mode", | ||
"main": "dist/cjs/index.js", | ||
@@ -30,2 +30,3 @@ "module": "dist/esm/index.js", | ||
"dependencies": { | ||
"@sitecore-jss/sitecore-jss": "^22.2.0-canary.38", | ||
"http-proxy-middleware": "^2.0.6", | ||
@@ -41,12 +42,18 @@ "http-status-codes": "^2.2.0", | ||
"@types/set-cookie-parser": "^2.4.2", | ||
"@types/supertest": "^6.0.2", | ||
"chai": "^4.3.7", | ||
"del-cli": "^5.0.0", | ||
"eslint": "^8.33.0", | ||
"express": "^4.19.2", | ||
"mocha": "^10.2.0", | ||
"nyc": "^15.1.0", | ||
"supertest": "^7.0.0", | ||
"ts-node": "^10.9.1", | ||
"typescript": "~4.9.5" | ||
}, | ||
"peerDependencies": { | ||
"express": "^4.19.2" | ||
}, | ||
"types": "types/index.d.ts", | ||
"gitHead": "2f18bc17c4728742b8ba81f1df2c237d4102d14c", | ||
"gitHead": "e232f6f998a3a411b5d7d7d3f5c7f61b90a21278", | ||
"files": [ | ||
@@ -53,0 +60,0 @@ "dist", |
# Sitecore JavaScript Rendering SDK Proxy | ||
This module is provided as a part of Sitecore JavaScript Rendering SDK (JSS). It contains the headless-mode SSR proxy implementation. | ||
This module is provided as a part of Sitecore JavaScript Rendering SDK (JSS). It provides middlewares, utilities to work in a headless mode. | ||
@@ -5,0 +5,0 @@ <!--- |
@@ -1,21 +0,2 @@ | ||
/// <reference types="qs" /> | ||
import { IncomingMessage } from 'http'; | ||
import { Request, RequestHandler } from 'express'; | ||
import { AppRenderer } from './AppRenderer'; | ||
import { ProxyConfig, ServerBundle } from './ProxyConfig'; | ||
import { RouteUrlParser } from './RouteUrlParser'; | ||
export declare const removeEmptyAnalyticsCookie: (proxyResponse: IncomingMessage) => void; | ||
/** | ||
* @param {string} reqPath | ||
* @param {Request} req | ||
* @param {ProxyConfig} config | ||
* @param {RouteUrlParser} parseRouteUrl | ||
*/ | ||
export declare function rewriteRequestPath(reqPath: string, req: Request, config: ProxyConfig, parseRouteUrl?: RouteUrlParser): string; | ||
/** | ||
* @param {AppRenderer} renderer | ||
* @param {ProxyConfig} config | ||
* @param {RouteUrlParser} parseRouteUrl | ||
*/ | ||
export default function scProxy(renderer: AppRenderer, config: ProxyConfig, parseRouteUrl: RouteUrlParser): RequestHandler<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>[]; | ||
export { ProxyConfig, ServerBundle }; | ||
export * from './middleware'; | ||
export * from './types'; |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
79526
33
1435
5
15
3
5
+ Added@sitecore-jss/sitecore-jss@22.3.1(transitive)
+ Addedaccepts@1.3.8(transitive)
+ Addedansi-styles@4.3.0(transitive)
+ Addedarray-flatten@1.1.1(transitive)
+ Addedasynckit@0.4.0(transitive)
+ Addedaxios@0.21.4(transitive)
+ Addedbody-parser@1.20.3(transitive)
+ Addedbytes@3.1.2(transitive)
+ Addedcall-bind-apply-helpers@1.0.1(transitive)
+ Addedcall-bound@1.0.3(transitive)
+ Addedchalk@4.1.2(transitive)
+ Addedcolor-convert@2.0.1(transitive)
+ Addedcolor-name@1.1.4(transitive)
+ Addedcombined-stream@1.0.8(transitive)
+ Addedcontent-disposition@0.5.4(transitive)
+ Addedcontent-type@1.0.5(transitive)
+ Addedcookie@0.7.1(transitive)
+ Addedcookie-signature@1.0.6(transitive)
+ Addedcross-fetch@3.2.0(transitive)
+ Addeddebug@2.6.94.4.0(transitive)
+ Addeddelayed-stream@1.0.0(transitive)
+ Addeddepd@2.0.0(transitive)
+ Addeddestroy@1.2.0(transitive)
+ Addeddunder-proto@1.0.1(transitive)
+ Addedee-first@1.1.1(transitive)
+ Addedencodeurl@1.0.22.0.0(transitive)
+ Addedes-define-property@1.0.1(transitive)
+ Addedes-errors@1.3.0(transitive)
+ Addedes-object-atoms@1.0.0(transitive)
+ Addedescape-html@1.0.3(transitive)
+ Addedetag@1.8.1(transitive)
+ Addedexpress@4.21.2(transitive)
+ Addedextract-files@9.0.0(transitive)
+ Addedfinalhandler@1.3.1(transitive)
+ Addedform-data@3.0.2(transitive)
+ Addedforwarded@0.2.0(transitive)
+ Addedfresh@0.5.2(transitive)
+ Addedfunction-bind@1.1.2(transitive)
+ Addedget-intrinsic@1.2.7(transitive)
+ Addedget-proto@1.0.1(transitive)
+ Addedgopd@1.2.0(transitive)
+ Addedgraphql@16.10.0(transitive)
+ Addedgraphql-request@4.3.0(transitive)
+ Addedhas-flag@4.0.0(transitive)
+ Addedhas-symbols@1.1.0(transitive)
+ Addedhasown@2.0.2(transitive)
+ Addedhttp-errors@2.0.0(transitive)
+ Addediconv-lite@0.4.24(transitive)
+ Addedinherits@2.0.4(transitive)
+ Addedipaddr.js@1.9.1(transitive)
+ Addedlodash.unescape@4.0.1(transitive)
+ Addedmath-intrinsics@1.1.0(transitive)
+ Addedmedia-typer@0.3.0(transitive)
+ Addedmemory-cache@0.2.0(transitive)
+ Addedmerge-descriptors@1.0.3(transitive)
+ Addedmethods@1.1.2(transitive)
+ Addedmime@1.6.0(transitive)
+ Addedmime-db@1.52.0(transitive)
+ Addedmime-types@2.1.35(transitive)
+ Addedms@2.0.02.1.3(transitive)
+ Addednegotiator@0.6.3(transitive)
+ Addednode-fetch@2.7.0(transitive)
+ Addedobject-inspect@1.13.3(transitive)
+ Addedon-finished@2.4.1(transitive)
+ Addedparseurl@1.3.3(transitive)
+ Addedpath-to-regexp@0.1.12(transitive)
+ Addedproxy-addr@2.0.7(transitive)
+ Addedqs@6.13.0(transitive)
+ Addedquerystringify@2.2.0(transitive)
+ Addedrange-parser@1.2.1(transitive)
+ Addedraw-body@2.5.2(transitive)
+ Addedsafe-buffer@5.2.1(transitive)
+ Addedsafer-buffer@2.1.2(transitive)
+ Addedsend@0.19.0(transitive)
+ Addedserve-static@1.16.2(transitive)
+ Addedsetprototypeof@1.2.0(transitive)
+ Addedside-channel@1.1.0(transitive)
+ Addedside-channel-list@1.0.0(transitive)
+ Addedside-channel-map@1.0.1(transitive)
+ Addedside-channel-weakmap@1.0.2(transitive)
+ Addedstatuses@2.0.1(transitive)
+ Addedsupports-color@7.2.0(transitive)
+ Addedtoidentifier@1.0.1(transitive)
+ Addedtr46@0.0.3(transitive)
+ Addedtype-is@1.6.18(transitive)
+ Addedunpipe@1.0.0(transitive)
+ Addedurl-parse@1.5.10(transitive)
+ Addedutils-merge@1.0.1(transitive)
+ Addedvary@1.1.2(transitive)
+ Addedwebidl-conversions@3.0.1(transitive)
+ Addedwhatwg-url@5.0.0(transitive)