@sitecore-jss/sitecore-jss-proxy
Advanced tools
Comparing version 22.3.0-canary.15 to 22.3.0
"use strict"; | ||
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]; } }; | ||
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 __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 */ | ||
} | ||
} | ||
} | ||
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); | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.GRAPHQL_LAYOUT_QUERY_NAME = void 0; | ||
__exportStar(require("./middleware"), exports); | ||
__exportStar(require("./types"), exports); | ||
__exportStar(require("./personalize"), exports); | ||
var layout_1 = require("@sitecore-jss/sitecore-jss/layout"); | ||
Object.defineProperty(exports, "GRAPHQL_LAYOUT_QUERY_NAME", { enumerable: true, get: function () { return layout_1.GRAPHQL_LAYOUT_QUERY_NAME; } }); | ||
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; |
@@ -1,4 +0,441 @@ | ||
export * from './middleware'; | ||
export * from './types'; | ||
export * from './personalize'; | ||
export { GRAPHQL_LAYOUT_QUERY_NAME } from '@sitecore-jss/sitecore-jss/layout'; | ||
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)]; | ||
} |
{ | ||
"name": "@sitecore-jss/sitecore-jss-proxy", | ||
"version": "22.3.0-canary.15", | ||
"description": "Middlewares, utilities to work in a headless mode", | ||
"version": "22.3.0", | ||
"description": "Proxy middleware for express.js server.", | ||
"main": "dist/cjs/index.js", | ||
@@ -12,9 +12,9 @@ "module": "dist/esm/index.js", | ||
"lint": "eslint \"./src/**/*.ts\"", | ||
"test": "mocha --require ts-node/register \"./src/**/*.test.ts\" --exit", | ||
"test": "mocha --require ts-node/register \"./src/**/*.test.ts\"", | ||
"prepublishOnly": "npm run build", | ||
"coverage": "nyc npm test", | ||
"generate-docs": "npx typedoc --plugin typedoc-plugin-markdown --readme none --entryPoints src/personalize/index.ts --out ../../ref-docs/sitecore-jss-proxy src/index.ts --githubPages false" | ||
"generate-docs": "npx typedoc --plugin typedoc-plugin-markdown --readme none --out ../../ref-docs/sitecore-jss-proxy src/index.ts --githubPages false" | ||
}, | ||
"engines": { | ||
"node": ">=22" | ||
"node": ">=20" | ||
}, | ||
@@ -31,5 +31,2 @@ "author": { | ||
"dependencies": { | ||
"@sitecore-cloudsdk/core": "^0.4.1", | ||
"@sitecore-cloudsdk/personalize": "^0.4.1", | ||
"@sitecore-jss/sitecore-jss": "^22.3.0-canary.15", | ||
"http-proxy-middleware": "^2.0.6", | ||
@@ -43,24 +40,14 @@ "http-status-codes": "^2.2.0", | ||
"@types/mocha": "^10.0.1", | ||
"@types/node": "^22.9.0", | ||
"@types/proxyquire": "^1.3.31", | ||
"@types/node": "^20.14.2", | ||
"@types/set-cookie-parser": "^2.4.2", | ||
"@types/sinon": "^17.0.3", | ||
"@types/supertest": "^6.0.2", | ||
"chai": "^4.3.7", | ||
"del-cli": "^5.0.0", | ||
"eslint": "^8.56.0", | ||
"express": "^4.19.2", | ||
"eslint": "^8.33.0", | ||
"mocha": "^10.2.0", | ||
"nyc": "^15.1.0", | ||
"proxyquire": "^2.1.3", | ||
"sinon": "^17.0.1", | ||
"supertest": "^7.0.0", | ||
"ts-node": "^10.9.1", | ||
"typescript": "~5.6.3" | ||
"typescript": "~4.9.5" | ||
}, | ||
"peerDependencies": { | ||
"express": "^4.19.2" | ||
}, | ||
"types": "types/index.d.ts", | ||
"gitHead": "3b2f6e4ee89a36797e65b3c41b5cbc13cb78d38c", | ||
"gitHead": "81b0239517966b6d0debdf6da3f12a98228c4d0c", | ||
"files": [ | ||
@@ -67,0 +54,0 @@ "dist", |
# Sitecore JavaScript Rendering SDK Proxy | ||
This module is provided as a part of Sitecore JavaScript Rendering SDK (JSS). It provides middlewares, utilities to work in a headless mode. | ||
This module is provided as a part of Sitecore JavaScript Rendering SDK (JSS). It contains the headless-mode SSR proxy implementation. | ||
@@ -5,0 +5,0 @@ <!--- |
@@ -1,4 +0,21 @@ | ||
export * from './middleware'; | ||
export * from './types'; | ||
export * from './personalize'; | ||
export { GRAPHQL_LAYOUT_QUERY_NAME } from '@sitecore-jss/sitecore-jss/layout'; | ||
/// <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 }; |
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
Deprecated
MaintenanceThe maintainer of the package marked it as deprecated. This could indicate that a single version should not be used, or that the package is no longer maintained and any new vulnerabilities will not be fixed.
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
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
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
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
3
12
0
1
64292
21
1079
1
5
- Removed@sitecore-cloudsdk/core@^0.4.1
- Removed@sitecore-cloudsdk/core@0.4.3(transitive)
- Removed@sitecore-cloudsdk/events@0.4.3(transitive)
- Removed@sitecore-cloudsdk/personalize@0.4.3(transitive)
- Removed@sitecore-cloudsdk/utils@0.4.3(transitive)
- Removed@sitecore-jss/sitecore-jss@22.3.1(transitive)
- Removedaccepts@1.3.8(transitive)
- Removedansi-styles@4.3.0(transitive)
- Removedarray-flatten@1.1.1(transitive)
- Removedasynckit@0.4.0(transitive)
- Removedaxios@0.21.4(transitive)
- Removedbody-parser@1.20.3(transitive)
- Removedbytes@3.1.2(transitive)
- Removedcall-bind-apply-helpers@1.0.1(transitive)
- Removedcall-bound@1.0.3(transitive)
- Removedchalk@4.1.2(transitive)
- Removedcolor-convert@2.0.1(transitive)
- Removedcolor-name@1.1.4(transitive)
- Removedcombined-stream@1.0.8(transitive)
- Removedcontent-disposition@0.5.4(transitive)
- Removedcontent-type@1.0.5(transitive)
- Removedcookie@0.7.1(transitive)
- Removedcookie-signature@1.0.6(transitive)
- Removedcross-fetch@3.2.0(transitive)
- Removeddebug@2.6.94.4.0(transitive)
- Removeddelayed-stream@1.0.0(transitive)
- Removeddepd@2.0.0(transitive)
- Removeddestroy@1.2.0(transitive)
- Removeddunder-proto@1.0.1(transitive)
- Removedee-first@1.1.1(transitive)
- Removedencodeurl@1.0.22.0.0(transitive)
- Removedes-define-property@1.0.1(transitive)
- Removedes-errors@1.3.0(transitive)
- Removedes-object-atoms@1.0.0(transitive)
- Removedescape-html@1.0.3(transitive)
- Removedetag@1.8.1(transitive)
- Removedexpress@4.21.2(transitive)
- Removedextract-files@9.0.0(transitive)
- Removedfinalhandler@1.3.1(transitive)
- Removedform-data@3.0.2(transitive)
- Removedforwarded@0.2.0(transitive)
- Removedfresh@0.5.2(transitive)
- Removedfunction-bind@1.1.2(transitive)
- Removedget-intrinsic@1.2.6(transitive)
- Removedgopd@1.2.0(transitive)
- Removedgraphql@16.10.0(transitive)
- Removedgraphql-request@4.3.0(transitive)
- Removedhas-flag@4.0.0(transitive)
- Removedhas-symbols@1.1.0(transitive)
- Removedhasown@2.0.2(transitive)
- Removedhttp-errors@2.0.0(transitive)
- Removediconv-lite@0.4.24(transitive)
- Removedinherits@2.0.4(transitive)
- Removedipaddr.js@1.9.1(transitive)
- Removedlodash.unescape@4.0.1(transitive)
- Removedmath-intrinsics@1.1.0(transitive)
- Removedmedia-typer@0.3.0(transitive)
- Removedmemory-cache@0.2.0(transitive)
- Removedmerge-descriptors@1.0.3(transitive)
- Removedmethods@1.1.2(transitive)
- Removedmime@1.6.0(transitive)
- Removedmime-db@1.52.0(transitive)
- Removedmime-types@2.1.35(transitive)
- Removedms@2.0.02.1.3(transitive)
- Removednegotiator@0.6.3(transitive)
- Removednode-fetch@2.7.0(transitive)
- Removedobject-inspect@1.13.3(transitive)
- Removedon-finished@2.4.1(transitive)
- Removedparseurl@1.3.3(transitive)
- Removedpath-to-regexp@0.1.12(transitive)
- Removedproxy-addr@2.0.7(transitive)
- Removedqs@6.13.0(transitive)
- Removedquerystringify@2.2.0(transitive)
- Removedrange-parser@1.2.1(transitive)
- Removedraw-body@2.5.2(transitive)
- Removedsafe-buffer@5.2.1(transitive)
- Removedsafer-buffer@2.1.2(transitive)
- Removedsend@0.19.0(transitive)
- Removedserve-static@1.16.2(transitive)
- Removedsetprototypeof@1.2.0(transitive)
- Removedside-channel@1.1.0(transitive)
- Removedside-channel-list@1.0.0(transitive)
- Removedside-channel-map@1.0.1(transitive)
- Removedside-channel-weakmap@1.0.2(transitive)
- Removedstatuses@2.0.1(transitive)
- Removedsupports-color@7.2.0(transitive)
- Removedtoidentifier@1.0.1(transitive)
- Removedtr46@0.0.3(transitive)
- Removedtype-is@1.6.18(transitive)
- Removedunpipe@1.0.0(transitive)
- Removedurl-parse@1.5.10(transitive)
- Removedutils-merge@1.0.1(transitive)
- Removedvary@1.1.2(transitive)
- Removedwebidl-conversions@3.0.1(transitive)
- Removedwhatwg-url@5.0.0(transitive)