UNPKG

@revoloo/cypress6

Version:

Cypress.io end to end testing tool

128 lines (102 loc) 3.92 kB
import _ from 'lodash' import Debug from 'debug' import { rewriteJsSourceMapAsync } from './async-rewriters' import * as sourceMaps from './util/source-maps' import url from 'url' const debug = Debug('cypress:rewriter:deferred-source-map-cache') export type DeferredSourceMapRequest = { uniqueId: string url: string js?: string sourceMap?: any resHeaders?: any } const caseInsensitiveGet = (obj, lowercaseProperty) => { for (let key of Object.keys(obj)) { if (key.toLowerCase() === lowercaseProperty) { return obj[key] } } } const getSourceMapHeader = (headers) => { // sourcemap has precedence // @see https://searchfox.org/mozilla-central/rev/dc4560dcaafd79375b9411fdbbaaebb0a59a93ac/devtools/shared/DevToolsUtils.js#611-619 return caseInsensitiveGet(headers, 'sourcemap') || caseInsensitiveGet(headers, 'x-sourcemap') } /** * Holds on to data necessary to rewrite user JS to maybe generate a sourcemap at a later time, * potentially composed with the user's own sourcemap if one is present. * * The purpose of this is to avoid wasting CPU time and network I/O on generating, composing, and * sending a sourcemap along with every single rewritten JS snippet, since the source maps are * going to be unused and discarded most of the time. */ export class DeferredSourceMapCache { _idCounter = 0 requests: DeferredSourceMapRequest[] = [] requestLib: any constructor (requestLib) { this.requestLib = requestLib } defer = (request: DeferredSourceMapRequest) => { if (this._getRequestById(request.uniqueId)) { // prevent duplicate uniqueIds from ever existing throw new Error(`Deferred sourcemap key "${request.uniqueId}" is not unique`) } // remove existing requests for this URL since they will not be loaded again this._removeRequestsByUrl(request.url) this.requests.push(request) } _removeRequestsByUrl (url: string) { _.remove(this.requests, { url }) } _getRequestById (uniqueId: string) { return _.find(this.requests, { uniqueId }) } async _getInputSourceMap (request: DeferredSourceMapRequest, headers: any) { // prefer inline sourceMappingURL over headers const sourceMapUrl = sourceMaps.getMappingUrl(request.js!) || getSourceMapHeader(request.resHeaders) if (!sourceMapUrl) { return } // try to decode it as a base64 string const inline = sourceMaps.tryDecodeInlineUrl(sourceMapUrl) if (inline) { return inline } // try to load it from the web const req = { url: url.resolve(request.url, sourceMapUrl), // TODO: this assumes that the sourcemap is on the same base domain, so it's safe to send the same headers // the browser sent for this sourcemap request - but if sourcemap is on a different domain, this will not // be true. need to use browser's cookiejar instead. headers, timeout: 5000, } try { const { body } = await this.requestLib(req, true) return body } catch (error) { // eslint-disable-next-line no-console debug('got an error loading user-provided sourcemap, serving proxy-generated sourcemap only %o', { url: request.url, headers, error }) } } async resolve (uniqueId: string, headers: any) { const request = this._getRequestById(uniqueId) if (!request) { throw new Error(`Missing request with ID '${uniqueId}'`) } if (request.sourceMap) { return request.sourceMap } if (!request.js) { throw new Error('Missing JS for source map rewrite') } const inputSourceMap = await this._getInputSourceMap(request, headers) // cache the sourceMap so we don't need to regenerate it request.sourceMap = await rewriteJsSourceMapAsync(request.url, request.js, inputSourceMap) delete request.js // won't need this again delete request.resHeaders return request.sourceMap } }