Files
cypress/packages/rewriter/lib/deferred-source-map-cache.ts
Zach Bloomquist 6960f7cd78 Rewrite JS/HTML using AST-based approach (#5273)
* Add winPropAccessor to security.js, remove other replacers

* Add start of Cypress.resolveWindowReference

* Add regexes for dot and bracket access

* Some security_spec tests pass with new injection

* Add resolveWindowReference unit tests

* Old security_spec now passes with resolveWindowReference

* Inject stub resolveWindowReference so proxy still works outside of Cypress

* wip: rewrite HTML + JS with tokenizer

* Move to using esprima + hyntax to rewrite JS + HTML

* remove comment; oneLine makes the whole thing commented

* Fix tests, apple.com edge case

* wip: add getOrSet

* Revert "wip: add getOrSet"

This reverts commit a5c647c00f.

* release 3.5.0 [skip ci]

* use recast to replace window property accesses

* replace assignments to top properly

* fix yarn.lock

* bump deps

* update integration tests

* remove old security ts?

* fix integration spec

* always ignore js interception failure

* use globalThis instead of window

* add experimentalSourceRewriting flag

* restore regex-writer spec

* fix types

* update config_spec

* add source rewriting spec

* cleanup

* simplify rewriting logic, move rules into rewriter package

* create threaded rewriting tool for non-streaming use

* update @packages/rewriter to use threads for async

* use async rewriting where convenient

* add worker-shim.js

* add performance info to debug logs

* properly handle +=, -=, ...

* add proxy, rewriter to unit-tests stage

* cleanup

* use parse5 to rewrite HTML, strip SRI

* update tests

* reorganization, cleanup

* rewrite ALL parent, top identifiers except in a few cases

* handle many JS edge cases

* ensure parse5@5.1.1 is installed

* update yarn.lock

* update tests

* add debugging, add tests

* add attempted repro for .href issue

* implement source maps + extending inline source maps

* update opts passing in proxy layer

* fix sourcemap naming structure

* update tests to account for sourcemaps

* sourcemap tests

* remote source maps work

* comment

* update rewriter tests

* clean up TODOs in resolveWindowReference

* remove @types/nock

* clean up todos in deferred-source-map-cache

* fix rewriter build script

* fix concatStream import

* bump expectedresultcount

* clean up js-rules

* threading improvements, workaround for Electron segfault

* no visit_spec for now

* fix 6_visit_spec

* update MAX_WORKER_THREADS

* add repro for #3975

* cleanup

* cleanup

* make better use of namedTypes and builders

* get rid of the horrific closureDetectionTernary

ast-types keeps track of scope, so it is unneeded

* fix #3975, #3994

* add x-sourcemap, sourcemap header support

* snap-shot-it 7.9.3

* add deferred-source-map-cache-spec

* add tests

* Throw error in driver if AST rewriting fails

* Fix "location = 'relative-url'"

* fix max recursion depth

* slim down some fixtures

* fix window.location usage

* don't mess with `frames` at all

* no integration tests

* skip testing apple.com for now

* update wording: regex-based vs. ast-based

* skip real-world tests for now

* add some padding to process.exit workaround

* fix resolvers_spec

* fix html-spec

* cleanup

* Update packages/rewriter/lib/js-rules.ts

* Update packages/driver/src/cypress/resolvers.ts

* just import find by itself

* privatize typedefs for Cypress.state, remove .gitignore, remove dead code

Co-authored-by: Ben Kucera <14625260+Bkucera@users.noreply.github.com>
2020-05-11 12:54:14 -04:00

128 lines
3.8 KiB
TypeScript

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
}
}