diff --git a/.circleci/cache-version.txt b/.circleci/cache-version.txt index 4a355f62e1a3..fc3ae1411bde 100644 --- a/.circleci/cache-version.txt +++ b/.circleci/cache-version.txt @@ -1,3 +1,3 @@ # Bump this version to force CI to re-create the cache from scratch. -10-31-22 +12-01-22 diff --git a/.circleci/workflows.yml b/.circleci/workflows.yml index 50fb4246edbf..2b21aece66fc 100644 --- a/.circleci/workflows.yml +++ b/.circleci/workflows.yml @@ -28,7 +28,7 @@ mainBuildFilters: &mainBuildFilters only: - develop - /^release\/\d+\.\d+\.\d+$/ - - 'feature/run-all-specs' + - 'ryanm/fix/v8-improvements' # usually we don't build Mac app - it takes a long time # but sometimes we want to really confirm we are doing the right thing @@ -37,7 +37,7 @@ macWorkflowFilters: &darwin-workflow-filters when: or: - equal: [ develop, << pipeline.git.branch >> ] - - equal: [ 'feature/run-all-specs', << pipeline.git.branch >> ] + - equal: [ 'ryanm/fix/v8-improvements', << pipeline.git.branch >> ] - matches: pattern: /^release\/\d+\.\d+\.\d+$/ value: << pipeline.git.branch >> @@ -45,7 +45,7 @@ linuxArm64WorkflowFilters: &linux-arm64-workflow-filters when: or: - equal: [ develop, << pipeline.git.branch >> ] - - equal: [ 'feature/run-all-specs', << pipeline.git.branch >> ] + - equal: [ 'ryanm/fix/v8-improvements', << pipeline.git.branch >> ] - matches: pattern: /^release\/\d+\.\d+\.\d+$/ value: << pipeline.git.branch >> @@ -63,6 +63,7 @@ windowsWorkflowFilters: &windows-workflow-filters when: or: - equal: [ develop, << pipeline.git.branch >> ] + - equal: [ 'ryanm/fix/v8-improvements', << pipeline.git.branch >> ] - matches: pattern: /^release\/\d+\.\d+\.\d+$/ value: << pipeline.git.branch >> @@ -128,7 +129,7 @@ commands: - run: name: Check current branch to persist artifacts command: | - if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* ]]; then + if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "ryanm/fix/v8-improvements" ]]; then echo "Not uploading artifacts or posting install comment for this branch." circleci-agent step halt fi @@ -1928,7 +1929,7 @@ jobs: <<: *defaultsParameters resource_class: type: string - default: large + default: xlarge resource_class: << parameters.resource_class >> steps: - restore_cached_workspace diff --git a/package.json b/package.json index 76cb307219ba..60636530ce27 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Cypress is a next generation front end testing tool built for the modern web", "private": true, "scripts": { - "binary-build": "node ./scripts/binary.js build", + "binary-build": "cross-env NODE_OPTIONS=--max_old_space_size=8192 node ./scripts/binary.js build", "binary-deploy": "node ./scripts/binary.js deploy", "binary-deploy-linux": "./scripts/build-linux-binary.sh", "binary-ensure": "node ./scripts/binary.js ensure", @@ -70,6 +70,7 @@ "prepare": "husky install" }, "dependencies": { + "bytenode": "1.3.7", "nvm": "0.0.4" }, "devDependencies": { @@ -140,6 +141,7 @@ "commander": "6.2.1", "common-tags": "1.8.0", "conventional-recommended-bump": "6.1.0", + "cross-env": "7.0.3", "debug": "^4.3.2", "dedent": "^0.7.0", "del": "3.0.0", diff --git a/packages/rewriter/script/worker-shim.js b/packages/rewriter/script/worker-shim.js index 2a61bf648282..50d770039d09 100644 --- a/packages/rewriter/script/worker-shim.js +++ b/packages/rewriter/script/worker-shim.js @@ -3,13 +3,8 @@ if (process.env.CYPRESS_INTERNAL_ENV === 'production') { throw new Error(`${__filename} should only run outside of prod`) } -if (require.name !== 'customRequire') { - // Purposefully make this a dynamic require so that it doesn't have the potential to get picked up by snapshotting mechanism - const hook = './hook' +const { hookRequire } = require('@packages/server/hook-require') - const { hookRequire } = require(`@packages/server/${hook}-require`) - - hookRequire(true) -} +hookRequire({ forceTypeScript: true }) require('../lib/threads/worker.ts') diff --git a/packages/server/hook-require.js b/packages/server/hook-require.js index a328364fabf5..1743f4d6b31c 100644 --- a/packages/server/hook-require.js +++ b/packages/server/hook-require.js @@ -9,7 +9,7 @@ function runWithSnapshot (forceTypeScript) { const { snapshotRequire } = require('@packages/v8-snapshot-require') const projectBaseDir = process.env.PROJECT_BASE_DIR - const supportTS = forceTypeScript || typeof global.snapshotResult === 'undefined' || global.supportTypeScript + const supportTS = forceTypeScript || typeof global.getSnapshotResult === 'undefined' || global.supportTypeScript snapshotRequire(projectBaseDir, { diagnosticsEnabled: isDev, @@ -30,8 +30,8 @@ function runWithSnapshot (forceTypeScript) { }) } -const hookRequire = (forceTypeScript) => { - if (['1', 'true'].includes(process.env.DISABLE_SNAPSHOT_REQUIRE) || typeof snapshotResult === 'undefined') { +const hookRequire = ({ forceTypeScript }) => { + if (['1', 'true'].includes(process.env.DISABLE_SNAPSHOT_REQUIRE) || typeof getSnapshotResult === 'undefined') { require('@packages/ts/register') } else { runWithSnapshot(forceTypeScript) diff --git a/packages/server/index.js b/packages/server/index.js index ad10f8c50a7b..56d877ed6f89 100644 --- a/packages/server/index.js +++ b/packages/server/index.js @@ -1,18 +1,19 @@ const { initializeStartTime } = require('./lib/util/performance_benchmark') -const run = async () => { - initializeStartTime() +const startCypress = async () => { + try { + initializeStartTime() - if (require.name !== 'customRequire') { - // Purposefully make this a dynamic require so that it doesn't have the potential to get picked up by snapshotting mechanism - const hook = './hook' + const { hookRequire } = require('./hook-require') - const { hookRequire } = require(`${hook}-require`) + hookRequire({ forceTypeScript: false }) - hookRequire(false) + await require('./start-cypress') + } catch (error) { + // eslint-disable-next-line no-console + console.error(error) + process.exit(1) } - - await require('./server-entry') } -module.exports = run() +module.exports = startCypress() diff --git a/packages/server/package.json b/packages/server/package.json index 81f342bef244..864e897a08fb 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -194,10 +194,11 @@ }, "files": [ "config", + "hook-require.js", "lib", "patches", - "server-entry.js", - "hook-require.js" + "start-cypress.js", + "v8-snapshot-entry.js" ], "types": "index.d.ts", "productName": "Cypress", diff --git a/packages/server/server-entry.js b/packages/server/start-cypress.js similarity index 100% rename from packages/server/server-entry.js rename to packages/server/start-cypress.js diff --git a/packages/server/v8-snapshot-entry.js b/packages/server/v8-snapshot-entry.js new file mode 100644 index 000000000000..30ea9fd9bf3f --- /dev/null +++ b/packages/server/v8-snapshot-entry.js @@ -0,0 +1 @@ +require('./start-cypress') diff --git a/packages/ts/registerDir.js b/packages/ts/registerDir.js index 51800a1634f3..b82bb0dca496 100644 --- a/packages/ts/registerDir.js +++ b/packages/ts/registerDir.js @@ -8,8 +8,8 @@ const path = require('path') // build has been done correctly module.exports = function (scopeDir) { // Only set up ts-node if we're not using the snapshot - // @ts-ignore snapshotResult is a global defined in the v8 snapshot - if (['1', 'true'].includes(process.env.DISABLE_SNAPSHOT_REQUIRE) || typeof snapshotResult === 'undefined') { + // @ts-ignore getSnapshotResult is a global defined in the v8 snapshot + if (['1', 'true'].includes(process.env.DISABLE_SNAPSHOT_REQUIRE) || typeof getSnapshotResult === 'undefined') { try { // Prevent double-compiling if we're testing the app and already have ts-node hook installed // TODO(tim): e2e testing does not like this, I guess b/c it's currently using the tsconfig diff --git a/packages/v8-snapshot-require/src/snapshot-require.ts b/packages/v8-snapshot-require/src/snapshot-require.ts index f9c173f637fe..83006733c1c6 100644 --- a/packages/v8-snapshot-require/src/snapshot-require.ts +++ b/packages/v8-snapshot-require/src/snapshot-require.ts @@ -181,8 +181,8 @@ export function snapshotRequire ( // 1. Assign snapshot which is a global if it was embedded const sr: Snapshot = opts.snapshotOverride || - // @ts-ignore global snapshotResult - (typeof snapshotResult !== 'undefined' ? snapshotResult : undefined) + // @ts-ignore global getSnapshotResult + (typeof getSnapshotResult !== 'undefined' ? getSnapshotResult() : undefined) // If we have no snapshot we don't need to hook anything if (sr != null || alwaysHook) { @@ -239,10 +239,7 @@ export function snapshotRequire ( moduleNeedsReload, }) - // @ts-ignore global snapshotResult - // 8. Ensure that the user passed the project base dir since the loader - // cannot resolve modules without it - if (typeof snapshotResult !== 'undefined') { + if (typeof sr !== 'undefined') { const projectBaseDir = process.env.PROJECT_BASE_DIR if (projectBaseDir == null) { @@ -290,8 +287,8 @@ export function snapshotRequire ( // 11. Inject those globals - // @ts-ignore global snapshotResult - snapshotResult.setGlobals( + // @ts-ignore setGlobals is a function on global sr + sr.setGlobals( global, checked_process, checked_window, @@ -305,8 +302,8 @@ export function snapshotRequire ( // @ts-ignore private module var require.cache = Module._cache - // @ts-ignore global snapshotResult - snapshotResult.customRequire.cache = require.cache + // @ts-ignore customRequire is a property of global sr + sr.customRequire.cache = require.cache // 12. Add some 'magic' functions that we can use from inside the // snapshot in order to integrate module loading diff --git a/scripts/after-pack-hook.js b/scripts/after-pack-hook.js index 239314877b83..1a2f9a83a391 100644 --- a/scripts/after-pack-hook.js +++ b/scripts/after-pack-hook.js @@ -6,7 +6,8 @@ const os = require('os') const path = require('path') const { setupV8Snapshots } = require('@tooling/v8-snapshot') const { flipFuses, FuseVersion, FuseV1Options } = require('@electron/fuses') -const { cleanup } = require('./binary/binary-cleanup') +const { buildEntryPointAndCleanup } = require('./binary/binary-cleanup') +const { getIntegrityCheckSource, getBinaryEntryPointSource } = require('./binary/binary-sources') module.exports = async function (params) { console.log('****************************') @@ -55,6 +56,8 @@ module.exports = async function (params) { } if (!['1', 'true'].includes(process.env.DISABLE_SNAPSHOT_REQUIRE)) { + await fs.writeFile(path.join(outputFolder, 'index.js'), getBinaryEntryPointSource()) + await flipFuses( exePathPerPlatform[os.platform()], { @@ -63,7 +66,11 @@ module.exports = async function (params) { }, ) - await setupV8Snapshots(params.appOutDir) - await cleanup(outputFolder) + // Build out the entry point and clean up prior to setting up v8 snapshots so that the state of the binary is correct + await buildEntryPointAndCleanup(outputFolder) + await setupV8Snapshots({ + cypressAppPath: params.appOutDir, + integrityCheckSource: getIntegrityCheckSource(outputFolder), + }) } } diff --git a/scripts/binary/binary-cleanup.js b/scripts/binary/binary-cleanup.js index dc6a70545baf..0b985fe1de9e 100644 --- a/scripts/binary/binary-cleanup.js +++ b/scripts/binary/binary-cleanup.js @@ -6,6 +6,7 @@ const esbuild = require('esbuild') const snapshotMetadata = require('@tooling/v8-snapshot/cache/prod-darwin/snapshot-meta.cache.json') const tempDir = require('temp-dir') const workingDir = path.join(tempDir, 'binary-cleanup-workdir') +const bytenode = require('bytenode') fs.ensureDirSync(workingDir) @@ -39,8 +40,6 @@ async function removeEmptyDirectories (directory) { const getDependencyPathsToKeep = async (buildAppDir) => { const unixBuildAppDir = buildAppDir.split(path.sep).join(path.posix.sep) const startingEntryPoints = [ - 'packages/server/index.js', - 'packages/server/hook-require.js', 'packages/server/lib/plugins/child/require_async_child.js', 'packages/server/lib/plugins/child/register_ts_node.js', 'packages/rewriter/lib/threads/worker.js', @@ -81,7 +80,7 @@ const getDependencyPathsToKeep = async (buildAppDir) => { absWorkingDir: unixBuildAppDir, external: [ './transpile-ts', - './server-entry', + './start-cypress', 'fsevents', 'pnpapi', '@swc/core', @@ -111,18 +110,63 @@ const getDependencyPathsToKeep = async (buildAppDir) => { }) } - return [...Object.keys(esbuildResult.metafile.inputs), ...entryPoints] + return [...Object.keys(esbuildResult.metafile.inputs), ...entryPoints, 'package.json'] } -const cleanup = async (buildAppDir) => { - // 1. Retrieve all dependencies that still need to be kept in the binary. In theory, we could use the bundles generated here as single files within the binary, - // but for now, we just track on the dependencies that get pulled in - const keptDependencies = [...await getDependencyPathsToKeep(buildAppDir), 'package.json', 'packages/server/server-entry.js'] +const createServerEntryPointBundle = async (buildAppDir) => { + const unixBuildAppDir = buildAppDir.split(path.sep).join(path.posix.sep) + const entryPoints = [path.join(unixBuildAppDir, 'packages/server/index.js')] + // Build the binary entry point ignoring anything that happens in start-cypress since that will be in the v8 snapshot + const esbuildResult = await esbuild.build({ + entryPoints, + bundle: true, + outdir: workingDir, + platform: 'node', + metafile: true, + absWorkingDir: unixBuildAppDir, + external: [ + './transpile-ts', + './start-cypress', + ], + }) + + console.log(`copying server entry point bundle from ${path.join(workingDir, 'index.js')} to ${path.join(buildAppDir, 'packages', 'server', 'index.js')}`) + + await fs.copy(path.join(workingDir, 'index.js'), path.join(buildAppDir, 'packages', 'server', 'index.js')) + + console.log(`compiling server entry point bundle to ${path.join(buildAppDir, 'packages', 'server', 'index.jsc')}`) + + // Use bytenode to compile the entry point bundle. This will save time on the v8 compile step and ensure the integrity of the entry point + await bytenode.compileFile({ + filename: path.join(buildAppDir, 'packages', 'server', 'index.js'), + output: path.join(buildAppDir, 'packages', 'server', 'index.jsc'), + electron: true, + }) + + // Convert these inputs to a relative file path. Note that these paths are posix paths. + return [...Object.keys(esbuildResult.metafile.inputs)].map((input) => `./${input}`) +} + +const buildEntryPointAndCleanup = async (buildAppDir) => { + const [keptDependencies, serverEntryPointBundleDependencies] = await Promise.all([ + // 1. Retrieve all dependencies that still need to be kept in the binary. In theory, we could use the bundles generated here as single files within the binary, + // but for now, we just track on the dependencies that get pulled in + getDependencyPathsToKeep(buildAppDir), + // 2. Create a bundle for the server entry point. This will be used to start the server in the binary. It returns the dependencies that are pulled in by this bundle that potentially can now be removed + createServerEntryPointBundle(buildAppDir), + ]) + + // 3. Gather the dependencies that could potentially be removed from the binary due to being in the snapshot or in the entry point bundle + const potentiallyRemovedDependencies = [ + ...snapshotMetadata.healthy, + ...snapshotMetadata.deferred, + ...snapshotMetadata.norewrite, + ...serverEntryPointBundleDependencies, + ] - // 2. Gather the dependencies that could potentially be removed from the binary due to being in the snapshot - const potentiallyRemovedDependencies = [...snapshotMetadata.healthy, ...snapshotMetadata.deferred, ...snapshotMetadata.norewrite] + console.log(`potentially removing ${potentiallyRemovedDependencies.length} dependencies`) - // 3. Remove all dependencies that are in the snapshot but not in the list of kept dependencies from the binary + // 4. Remove all dependencies that are in the snapshot but not in the list of kept dependencies from the binary await Promise.all(potentiallyRemovedDependencies.map(async (dependency) => { const typeScriptlessDependency = dependency.replace(/\.ts$/, '.js') @@ -132,10 +176,10 @@ const cleanup = async (buildAppDir) => { } })) - // 4. Consolidate dependencies that are safe to consolidate (`lodash` and `bluebird`) + // 5. Consolidate dependencies that are safe to consolidate (`lodash` and `bluebird`) await consolidateDeps({ projectBaseDir: buildAppDir }) - // 5. Remove various unnecessary files from the binary to further clean things up. Likely, there is additional work that can be done here + // 6. Remove various unnecessary files from the binary to further clean things up. Likely, there is additional work that can be done here await del([ // Remove test files path.join(buildAppDir, '**', 'test'), @@ -187,10 +231,10 @@ const cleanup = async (buildAppDir) => { path.join(buildAppDir, '**', 'yarn.lock'), ], { force: true }) - // 6. Remove any empty directories as a result of the rest of the cleanup + // 7. Remove any empty directories as a result of the rest of the cleanup await removeEmptyDirectories(buildAppDir) } module.exports = { - cleanup, + buildEntryPointAndCleanup, } diff --git a/scripts/binary/binary-entry-point-source.js b/scripts/binary/binary-entry-point-source.js new file mode 100644 index 000000000000..ddf0469eeebf --- /dev/null +++ b/scripts/binary/binary-entry-point-source.js @@ -0,0 +1,21 @@ +const Module = require('module') +const path = require('path') + +process.env.CYPRESS_INTERNAL_ENV = process.env.CYPRESS_INTERNAL_ENV || 'production' +try { + require('./node_modules/bytenode/lib/index.js') + const filename = path.join(__dirname, 'packages', 'server', 'index.jsc') + const dirname = path.dirname(filename) + + Module._extensions['.jsc']({ + require: module.require, + id: filename, + filename, + loaded: false, + path: dirname, + paths: Module._nodeModulePaths(dirname), + }, filename) +} catch (error) { + console.error(error) + process.exit(1) +} diff --git a/scripts/binary/binary-integrity-check-source.js b/scripts/binary/binary-integrity-check-source.js new file mode 100644 index 000000000000..cec2c9316ca9 --- /dev/null +++ b/scripts/binary/binary-integrity-check-source.js @@ -0,0 +1,184 @@ +const OrigError = Error +const captureStackTrace = Error.captureStackTrace +const toString = Function.prototype.toString +const callFn = Function.call + +const integrityErrorMessage = ` +We detected an issue with the integrity of the Cypress binary. It may have been modified and cannot run. We recommend re-installing the Cypress binary with: + +\`cypress cache clear && cypress install\` +` + +const stackIntegrityCheck = function stackIntegrityCheck (options) { + const originalStackTraceLimit = OrigError.stackTraceLimit + const originalPrepareStackTrace = OrigError.prepareStackTrace + + OrigError.stackTraceLimit = Infinity + + OrigError.prepareStackTrace = function (_, stack) { + return stack + } + + const tempError = new OrigError + + captureStackTrace(tempError, arguments.callee) + const stack = tempError.stack.filter((frame) => !frame.getFileName().startsWith('node:internal') && !frame.getFileName().startsWith('node:electron')) + + OrigError.prepareStackTrace = originalPrepareStackTrace + OrigError.stackTraceLimit = originalStackTraceLimit + + if (stack.length !== options.stackToMatch.length) { + console.error(`Integrity check failed with expected stack length ${options.stackToMatch.length} but got ${stack.length}`) + throw new Error(integrityErrorMessage) + } + + for (let index = 0; index < options.stackToMatch.length; index++) { + const { functionName: expectedFunctionName, fileName: expectedFileName } = options.stackToMatch[index] + const actualFunctionName = stack[index].getFunctionName() + const actualFileName = stack[index].getFileName() + + if (expectedFunctionName && actualFunctionName !== expectedFunctionName) { + console.error(`Integrity check failed with expected function name ${expectedFunctionName} but got ${actualFunctionName}`) + throw new Error(integrityErrorMessage) + } + + if (expectedFileName && actualFileName !== expectedFileName) { + console.error(`Integrity check failed with expected file name ${expectedFileName} but got ${actualFileName}`) + throw new Error(integrityErrorMessage) + } + } +} + +function validateToString () { + if (toString.call !== callFn) { + console.error(`Integrity check failed for toString.call`) + throw new Error('Integrity check failed for toString.call') + } +} + +function validateElectron (electron) { + // Hard coded function as this is electron code and there's not an easy way to get the function string at package time. If this fails on an updated version of electron, we'll need to update this. + if (toString.call(electron.app.getAppPath) !== 'function getAppPath() { [native code] }') { + console.error(`Integrity check failed for toString.call(electron.app.getAppPath)`) + throw new Error(`Integrity check failed for toString.call(electron.app.getAppPath)`) + } +} + +function validateFs (fs) { + // Hard coded function as this is electron code and there's not an easy way to get the function string at package time. If this fails on an updated version of electron, we'll need to update this. + if (toString.call(fs.readFileSync) !== `function(t,r){const n=splitPath(t);if(!n.isAsar)return g.apply(this,arguments);const{asarPath:i,filePath:a}=n,o=getOrCreateArchive(i);if(!o)throw createError("INVALID_ARCHIVE",{asarPath:i});const c=o.getFileInfo(a);if(!c)throw createError("NOT_FOUND",{asarPath:i,filePath:a});if(0===c.size)return r?"":s.Buffer.alloc(0);if(c.unpacked){const t=o.copyFileOut(a);return e.readFileSync(t,r)}if(r){if("string"==typeof r)r={encoding:r};else if("object"!=typeof r)throw new TypeError("Bad arguments")}else r={encoding:null};const{encoding:f}=r,l=s.Buffer.alloc(c.size),u=o.getFdAndValidateIntegrityLater();if(!(u>=0))throw createError("NOT_FOUND",{asarPath:i,filePath:a});return logASARAccess(i,a,c.offset),e.readSync(u,l,0,c.size,c.offset),validateBufferIntegrity(l,c.integrity),f?l.toString(f):l}`) { + console.error(`Integrity check failed for toString.call(fs.readFileSync)`) + throw new Error(integrityErrorMessage) + } +} + +function validateCrypto (crypto) { + if (toString.call(crypto.createHmac) !== `CRYPTO_CREATE_HMAC_TO_STRING`) { + console.error(`Integrity check failed for toString.call(crypto.createHmac)`) + throw new Error(integrityErrorMessage) + } + + if (toString.call(crypto.Hmac.prototype.update) !== `CRYPTO_HMAC_UPDATE_TO_STRING`) { + console.error(`Integrity check failed for toString.call(crypto.Hmac.prototype.update)`) + throw new Error(integrityErrorMessage) + } + + if (toString.call(crypto.Hmac.prototype.digest) !== `CRYPTO_HMAC_DIGEST_TO_STRING`) { + console.error(`Integrity check failed for toString.call(crypto.Hmac.prototype.digest)`) + throw new Error(integrityErrorMessage) + } +} + +function validateFile ({ filePath, crypto, fs, expectedHash, errorMessage }) { + const hash = crypto.createHmac('md5', 'HMAC_SECRET').update(fs.readFileSync(filePath, 'utf8')).digest('hex') + + if (hash !== expectedHash) { + console.error(errorMessage) + throw new Error(integrityErrorMessage) + } +} + +// eslint-disable-next-line no-unused-vars +function integrityCheck (options) { + const require = options.require + const electron = require('electron') + const fs = require('fs') + const crypto = require('crypto') + + // 1. Validate that the native functions we are using haven't been tampered with + validateToString() + validateElectron(electron) + validateFs(fs) + validateCrypto(crypto) + + const appPath = electron.app.getAppPath() + + // 2. Validate that the stack trace is what we expect + stackIntegrityCheck({ stackToMatch: + [ + { + functionName: 'integrityCheck', + fileName: '', + }, + { + fileName: '', + }, + { + functionName: 'snapshotRequire', + fileName: 'evalmachine.', + }, + { + functionName: 'runWithSnapshot', + fileName: 'evalmachine.', + }, + { + functionName: 'hookRequire', + fileName: 'evalmachine.', + }, + { + functionName: 'startCypress', + fileName: 'evalmachine.', + }, + { + fileName: 'evalmachine.', + }, + { + functionName: 'Module._extensions.', + // eslint-disable-next-line no-undef + fileName: [appPath, 'node_modules', 'bytenode', 'lib', 'index.js'].join(PATH_SEP), + }, + { + // eslint-disable-next-line no-undef + fileName: [appPath, 'index.js'].join(PATH_SEP), + }, + ], + }) + + // 3. Validate the three pieces of the entry point: the main index file, the bundled jsc file, and the bytenode node module + validateFile({ + // eslint-disable-next-line no-undef + filePath: [appPath, 'index.js'].join(PATH_SEP), + crypto, + fs, + expectedHash: 'MAIN_INDEX_HASH', + errorMessage: 'Integrity check failed for main index.js file', + }) + + validateFile({ + // eslint-disable-next-line no-undef + filePath: [appPath, 'node_modules', 'bytenode', 'lib', 'index.js'].join(PATH_SEP), + crypto, + fs, + expectedHash: 'BYTENODE_HASH', + errorMessage: 'Integrity check failed for main bytenode.js file', + }) + + validateFile({ + // eslint-disable-next-line no-undef + filePath: [appPath, 'packages', 'server', 'index.jsc'].join(PATH_SEP), + crypto, + fs, + expectedHash: 'INDEX_JSC_HASH', + errorMessage: 'Integrity check failed for main server index.jsc file', + }) +} diff --git a/scripts/binary/binary-sources.js b/scripts/binary/binary-sources.js new file mode 100644 index 000000000000..077ff675dd28 --- /dev/null +++ b/scripts/binary/binary-sources.js @@ -0,0 +1,38 @@ +const fs = require('fs') +const crypto = require('crypto') +const path = require('path') + +const escapeString = (string) => string.replaceAll(`\``, `\\\``).replaceAll(`$`, `\\$`) + +function read (file) { + const pathToFile = require.resolve(`./${file}`) + + return fs.readFileSync(pathToFile, 'utf8') +} + +const getBinaryEntryPointSource = () => { + return read('binary-entry-point-source.js') +} + +const getIntegrityCheckSource = (baseDirectory) => { + const fileSource = read('binary-integrity-check-source.js') + const secret = require('crypto').randomBytes(48).toString('hex') + + const mainIndexHash = crypto.createHmac('md5', secret).update(fs.readFileSync(path.join(baseDirectory, './index.js'), 'utf8')).digest('hex') + const bytenodeHash = crypto.createHmac('md5', secret).update(fs.readFileSync(path.join(baseDirectory, './node_modules/bytenode/lib/index.js'), 'utf8')).digest('hex') + const indexJscHash = crypto.createHmac('md5', secret).update(fs.readFileSync(path.join(baseDirectory, './packages/server/index.jsc'), 'utf8')).digest('hex') + + return fileSource.split('\n').join(`\n `) + .replaceAll('MAIN_INDEX_HASH', mainIndexHash) + .replaceAll('BYTENODE_HASH', bytenodeHash) + .replaceAll('INDEX_JSC_HASH', indexJscHash) + .replaceAll('HMAC_SECRET', secret) + .replaceAll('CRYPTO_CREATE_HMAC_TO_STRING', escapeString(crypto.createHmac.toString())) + .replaceAll('CRYPTO_HMAC_UPDATE_TO_STRING', escapeString(crypto.Hmac.prototype.update.toString())) + .replaceAll('CRYPTO_HMAC_DIGEST_TO_STRING', escapeString(crypto.Hmac.prototype.digest.toString())) +} + +module.exports = { + getBinaryEntryPointSource, + getIntegrityCheckSource, +} diff --git a/scripts/binary/build.ts b/scripts/binary/build.ts index 5b39e33f5c1d..65a85334d4df 100644 --- a/scripts/binary/build.ts +++ b/scripts/binary/build.ts @@ -19,6 +19,7 @@ import execa from 'execa' import { testStaticAssets } from './util/testStaticAssets' import performanceTracking from '../../system-tests/lib/performance' import verify from '../../cli/lib/tasks/verify' +import * as electronBuilder from 'electron-builder' const globAsync = promisify(glob) @@ -174,12 +175,9 @@ export async function buildCypressApp (options: BuildCypressAppOpts) { }, { spaces: 2 }) fs.writeFileSync(meta.distDir('index.js'), `\ -${!['1', 'true'].includes(process.env.DISABLE_SNAPSHOT_REQUIRE) ? -`if (!global.snapshotResult && process.versions?.electron) { - throw new Error('global.snapshotResult is not defined. This binary has been built incorrectly.') -}` : ''} process.env.CYPRESS_INTERNAL_ENV = process.env.CYPRESS_INTERNAL_ENV || 'production' -require('./packages/server')\ +require('./node_modules/bytenode/lib/index.js') +require('./packages/server/index.js') `) // removeTypeScript @@ -237,23 +235,6 @@ require('./packages/server')\ console.log(`output folder: ${outputFolder}`) - const args = [ - '--publish=never', - `--c.electronVersion=${electronVersion}`, - `--c.directories.app=${appFolder}`, - `--c.directories.output=${outputFolder}`, - `--c.icon=${iconFilename}`, - // for now we cannot pack source files in asar file - // because electron-builder does not copy nested folders - // from packages/*/node_modules - // see https://github.com/electron-userland/electron-builder/issues/3185 - // so we will copy those folders later ourselves - '--c.asar=false', - ] - - console.log('electron-builder arguments:') - console.log(args.join(' ')) - // Update the root package.json with the next app version so that it is snapshot properly fs.writeJSONSync(path.join(CY_ROOT_DIR, 'package.json'), { ...jsonRoot, @@ -261,10 +242,21 @@ require('./packages/server')\ }, { spaces: 2 }) try { - await execa('electron-builder', args, { - stdio: 'inherit', - env: { - NODE_OPTIONS: '--max_old_space_size=8192', + await electronBuilder.build({ + publish: 'never', + config: { + electronVersion, + directories: { + app: appFolder, + output: outputFolder, + }, + icon: iconFilename, + // for now we cannot pack source files in asar file + // because electron-builder does not copy nested folders + // from packages/*/node_modules + // see https://github.com/electron-userland/electron-builder/issues/3185 + // so we will copy those folders later ourselves + asar: false, }, }) } catch (e) { @@ -298,7 +290,7 @@ require('./packages/server')\ const executablePath = meta.buildAppExecutable() - await smoke.test(executablePath) + await smoke.test(executablePath, meta.buildAppDir()) } finally { if (usingXvfb) { await xvfb.stop() diff --git a/scripts/binary/smoke.js b/scripts/binary/smoke.js index 72b17b259991..6438ca54e50d 100644 --- a/scripts/binary/smoke.js +++ b/scripts/binary/smoke.js @@ -201,7 +201,96 @@ const runV8SnapshotProjectTest = function (buildAppExecutable, e2e) { return spawn() } -const test = async function (buildAppExecutable) { +const runErroringProjectTest = function (buildAppExecutable, e2e, testName, errorMessage) { + return new Promise((resolve, reject) => { + const env = _.omit(process.env, 'CYPRESS_INTERNAL_ENV') + + if (!canRecordVideo()) { + console.log('cannot record video on this platform yet, disabling') + env.CYPRESS_VIDEO_RECORDING = 'false' + } + + const args = [ + `--run-project=${e2e}`, + `--spec=${e2e}/cypress/e2e/simple_passing.cy.js`, + ] + + if (verify.needsSandbox()) { + args.push('--no-sandbox') + } + + const options = { + stdio: ['inherit', 'inherit', 'pipe'], env, + } + + console.log('running project test') + console.log(buildAppExecutable, args.join(' ')) + + const childProcess = cp.spawn(buildAppExecutable, args, options) + let errorOutput = '' + + childProcess.stderr.on('data', (data) => { + errorOutput += data.toString() + }) + + childProcess.on('exit', (code) => { + if (code === 0) { + return reject(new Error(`running project tests should have failed for test: ${testName}`)) + } + + if (!errorOutput.includes(errorMessage)) { + return reject(new Error(`running project tests failed with errors: ${errorOutput} but did not include the expected error message: '${errorMessage}'`)) + } + + return resolve() + }) + }) +} + +const runIntegrityTest = async function (buildAppExecutable, buildAppDir, e2e) { + const testCorruptingFile = async (file, errorMessage) => { + const contents = await fs.readFile(file) + + // Backup state + await fs.move(file, `${file}.bak`) + + // Modify app + await fs.writeFile(file, Buffer.concat([contents, Buffer.from(`\nconsole.log('modified code')`)])) + await runErroringProjectTest(buildAppExecutable, e2e, `corrupting ${file}`, errorMessage) + + // Restore original state + await fs.move(`${file}.bak`, file, { overwrite: true }) + } + + await testCorruptingFile(path.join(buildAppDir, 'index.js'), 'Integrity check failed for main index.js file') + await testCorruptingFile(path.join(buildAppDir, 'packages', 'server', 'index.jsc'), 'Integrity check failed for main server index.jsc file') + await testCorruptingFile(path.join(buildAppDir, 'node_modules', 'bytenode', 'lib', 'index.js'), 'Integrity check failed for main bytenode.js file') + + const testAlteringEntryPoint = async (additionalCode, errorMessage) => { + const packageJsonContents = await fs.readJSON(path.join(buildAppDir, 'package.json')) + + // Backup state + await fs.move(path.join(buildAppDir, 'package.json'), path.join(buildAppDir, 'package.json.bak')) + + // Modify app + await fs.writeJSON(path.join(buildAppDir, 'package.json'), { + ...packageJsonContents, + main: 'index2.js', + }) + + await fs.writeFile(path.join(buildAppDir, 'index2.js'), `${additionalCode}\nrequire("./index.js")`) + await runErroringProjectTest(buildAppExecutable, e2e, 'altering entry point', errorMessage) + + // Restore original state + await fs.move(path.join(buildAppDir, 'package.json.bak'), path.join(buildAppDir, 'package.json'), { overwrite: true }) + await fs.remove(path.join(buildAppDir, 'index2.js')) + } + + await testAlteringEntryPoint('console.log("simple alteration")', 'Integrity check failed with expected stack length 9 but got 10') + await testAlteringEntryPoint('console.log("accessing " + global.getSnapshotResult())', 'getSnapshotResult can only be called once') +} + +const test = async function (buildAppExecutable, buildAppDir) { await scaffoldCommonNodeModules() await Fixtures.scaffoldProject('e2e') const e2e = Fixtures.projectPath('e2e') @@ -210,6 +299,7 @@ const test = async function (buildAppExecutable) { await runProjectTest(buildAppExecutable, e2e) await runFailingProjectTest(buildAppExecutable, e2e) if (!['1', 'true'].includes(process.env.DISABLE_SNAPSHOT_REQUIRE)) { + await runIntegrityTest(buildAppExecutable, buildAppDir, e2e) await runV8SnapshotProjectTest(buildAppExecutable, e2e) } diff --git a/tooling/v8-snapshot/cache/dev-darwin/snapshot-meta.cache.json b/tooling/v8-snapshot/cache/dev-darwin/snapshot-meta.cache.json index 7a6fc0a62b60..0ae4c87680fe 100644 --- a/tooling/v8-snapshot/cache/dev-darwin/snapshot-meta.cache.json +++ b/tooling/v8-snapshot/cache/dev-darwin/snapshot-meta.cache.json @@ -3542,5 +3542,5 @@ "./tooling/v8-snapshot/cache/dev-darwin/snapshot-entry.js" ], "deferredHashFile": "yarn.lock", - "deferredHash": "8d8b606dabd3cc9e96c061293fa33f345f7bb2e66024b018d2edac2302b09f31" + "deferredHash": "844da7908a41692a3b04716c88e2f0cdad85ece6f94f6ab89fbd1ffe5c332fd2" } \ No newline at end of file diff --git a/tooling/v8-snapshot/cache/dev-linux/snapshot-meta.cache.json b/tooling/v8-snapshot/cache/dev-linux/snapshot-meta.cache.json index 86e5a156d51c..913ff23a3c45 100644 --- a/tooling/v8-snapshot/cache/dev-linux/snapshot-meta.cache.json +++ b/tooling/v8-snapshot/cache/dev-linux/snapshot-meta.cache.json @@ -3541,5 +3541,5 @@ "./tooling/v8-snapshot/cache/dev-linux/snapshot-entry.js" ], "deferredHashFile": "yarn.lock", - "deferredHash": "8d8b606dabd3cc9e96c061293fa33f345f7bb2e66024b018d2edac2302b09f31" + "deferredHash": "844da7908a41692a3b04716c88e2f0cdad85ece6f94f6ab89fbd1ffe5c332fd2" } \ No newline at end of file diff --git a/tooling/v8-snapshot/cache/dev-win32/snapshot-meta.cache.json b/tooling/v8-snapshot/cache/dev-win32/snapshot-meta.cache.json index dfff2f7f3ece..0b4c1126cd13 100644 --- a/tooling/v8-snapshot/cache/dev-win32/snapshot-meta.cache.json +++ b/tooling/v8-snapshot/cache/dev-win32/snapshot-meta.cache.json @@ -33,6 +33,11 @@ "./node_modules/mocha/node_modules/debug/src/node.js", "./node_modules/morgan/node_modules/debug/src/node.js", "./node_modules/prettier/index.js", + "./node_modules/prettier/parser-babel.js", + "./node_modules/prettier/parser-espree.js", + "./node_modules/prettier/parser-flow.js", + "./node_modules/prettier/parser-meriyah.js", + "./node_modules/prettier/parser-typescript.js", "./node_modules/prettier/third-party.js", "./node_modules/send/node_modules/debug/src/node.js", "./node_modules/stream-parser/node_modules/debug/src/node.js", @@ -45,11 +50,20 @@ "./packages/https-proxy/lib/ca.js", "./packages/net-stubbing/node_modules/debug/src/node.js", "./packages/network/node_modules/minimatch/minimatch.js", + "./packages/server/lib/browsers/utils.ts", + "./packages/server/lib/cloud/exception.ts", + "./packages/server/lib/errors.ts", "./packages/server/lib/modes/record.js", "./packages/server/lib/modes/run.ts", + "./packages/server/lib/open_project.ts", + "./packages/server/lib/project-base.ts", + "./packages/server/lib/socket-ct.ts", + "./packages/server/lib/util/process_profiler.ts", "./packages/server/node_modules/@benmalka/foxdriver/node_modules/graceful-fs/polyfills.js", + "./packages/server/node_modules/ci-info/index.js", "./packages/server/node_modules/glob/node_modules/minimatch/minimatch.js", "./packages/server/node_modules/graceful-fs/polyfills.js", + "./packages/server/node_modules/is-ci/index.js", "./packages/server/node_modules/mocha/node_modules/debug/src/node.js", "./packages/server/node_modules/signal-exit/index.js", "./process-nextick-args/index.js", @@ -3424,6 +3438,7 @@ "./packages/server/node_modules/@benmalka/foxdriver/node_modules/graceful-fs/legacy-streams.js", "./packages/server/node_modules/@benmalka/foxdriver/package.json", "./packages/server/node_modules/ansi-regex/index.js", + "./packages/server/node_modules/ci-info/vendors.json", "./packages/server/node_modules/cli-table3/index.js", "./packages/server/node_modules/cli-table3/src/cell.js", "./packages/server/node_modules/cli-table3/src/layout-manager.js", @@ -3529,5 +3544,5 @@ "./tooling/v8-snapshot/cache/dev-win32/snapshot-entry.js" ], "deferredHashFile": "yarn.lock", - "deferredHash": "5a515f98fe67a8a56a035563ff9c8b9e4a9edc4d035907c3fa5fef1bb60f1dfc" + "deferredHash": "7a05f23c5bcd4b5daed5b113c4a56ec620c8686ee7d956edecb5b117e41903bb" } \ No newline at end of file diff --git a/tooling/v8-snapshot/cache/prod-darwin/snapshot-meta.cache.json b/tooling/v8-snapshot/cache/prod-darwin/snapshot-meta.cache.json index 145ff7f48721..b5b881125887 100644 --- a/tooling/v8-snapshot/cache/prod-darwin/snapshot-meta.cache.json +++ b/tooling/v8-snapshot/cache/prod-darwin/snapshot-meta.cache.json @@ -62,8 +62,10 @@ "./packages/server/lib/util/process_profiler.ts", "./packages/server/lib/util/suppress_warnings.js", "./packages/server/node_modules/@benmalka/foxdriver/node_modules/graceful-fs/polyfills.js", + "./packages/server/node_modules/ci-info/index.js", "./packages/server/node_modules/glob/node_modules/minimatch/minimatch.js", "./packages/server/node_modules/graceful-fs/polyfills.js", + "./packages/server/node_modules/is-ci/index.js", "./packages/server/node_modules/mocha/node_modules/debug/src/node.js", "./packages/server/node_modules/signal-exit/index.js", "./process-nextick-args/index.js", @@ -935,7 +937,8 @@ "./packages/server/node_modules/uuid/dist/v3.js", "./packages/server/node_modules/uuid/dist/v4.js", "./packages/server/node_modules/uuid/dist/v5.js", - "./packages/server/server-entry.js", + "./packages/server/start-cypress.js", + "./packages/server/v8-snapshot-entry.js", "./packages/socket/index.js", "./packages/socket/lib/socket.ts", "./packages/socket/node_modules/socket.io/dist/broadcast-operator.js", @@ -3807,6 +3810,7 @@ "./packages/server/node_modules/@benmalka/foxdriver/node_modules/graceful-fs/legacy-streams.js", "./packages/server/node_modules/@benmalka/foxdriver/package.json", "./packages/server/node_modules/ansi-regex/index.js", + "./packages/server/node_modules/ci-info/vendors.json", "./packages/server/node_modules/cli-table3/index.js", "./packages/server/node_modules/cli-table3/src/cell.js", "./packages/server/node_modules/cli-table3/src/layout-manager.js", @@ -3930,5 +3934,5 @@ "./tooling/v8-snapshot/cache/prod-darwin/snapshot-entry.js" ], "deferredHashFile": "yarn.lock", - "deferredHash": "8b71698b89d3804ed712295c20a140cfcd674fa5c3ad9569530282dd6c3e9906" + "deferredHash": "844da7908a41692a3b04716c88e2f0cdad85ece6f94f6ab89fbd1ffe5c332fd2" } \ No newline at end of file diff --git a/tooling/v8-snapshot/cache/prod-linux/snapshot-meta.cache.json b/tooling/v8-snapshot/cache/prod-linux/snapshot-meta.cache.json index 18b1a5233bb0..702984a5436e 100644 --- a/tooling/v8-snapshot/cache/prod-linux/snapshot-meta.cache.json +++ b/tooling/v8-snapshot/cache/prod-linux/snapshot-meta.cache.json @@ -57,13 +57,15 @@ "./packages/server/lib/modes/record.js", "./packages/server/lib/modes/run.ts", "./packages/server/lib/open_project.ts", - "./packages/server/lib/util/process_profiler.ts", "./packages/server/lib/project-base.ts", "./packages/server/lib/socket-ct.ts", + "./packages/server/lib/util/process_profiler.ts", "./packages/server/lib/util/suppress_warnings.js", "./packages/server/node_modules/@benmalka/foxdriver/node_modules/graceful-fs/polyfills.js", + "./packages/server/node_modules/ci-info/index.js", "./packages/server/node_modules/glob/node_modules/minimatch/minimatch.js", "./packages/server/node_modules/graceful-fs/polyfills.js", + "./packages/server/node_modules/is-ci/index.js", "./packages/server/node_modules/mocha/node_modules/debug/src/node.js", "./packages/server/node_modules/signal-exit/index.js", "./process-nextick-args/index.js", @@ -934,7 +936,8 @@ "./packages/server/node_modules/uuid/dist/v3.js", "./packages/server/node_modules/uuid/dist/v4.js", "./packages/server/node_modules/uuid/dist/v5.js", - "./packages/server/server-entry.js", + "./packages/server/start-cypress.js", + "./packages/server/v8-snapshot-entry.js", "./packages/socket/index.js", "./packages/socket/lib/socket.ts", "./packages/socket/node_modules/socket.io/dist/broadcast-operator.js", @@ -3643,6 +3646,7 @@ "./packages/net-stubbing/node_modules/mime-types/index.js", "./packages/network/lib/allow-destroy.ts", "./packages/network/lib/blocked.ts", + "./packages/network/lib/ca.ts", "./packages/network/lib/concat-stream.ts", "./packages/network/lib/http-utils.ts", "./packages/network/lib/index.ts", @@ -3805,6 +3809,7 @@ "./packages/server/node_modules/@benmalka/foxdriver/node_modules/graceful-fs/legacy-streams.js", "./packages/server/node_modules/@benmalka/foxdriver/package.json", "./packages/server/node_modules/ansi-regex/index.js", + "./packages/server/node_modules/ci-info/vendors.json", "./packages/server/node_modules/cli-table3/index.js", "./packages/server/node_modules/cli-table3/src/cell.js", "./packages/server/node_modules/cli-table3/src/layout-manager.js", @@ -3928,5 +3933,5 @@ "./tooling/v8-snapshot/cache/prod-linux/snapshot-entry.js" ], "deferredHashFile": "yarn.lock", - "deferredHash": "95205f49259fe2d246d45ef15d1499f6e3d1d235d6db892124bbd5423f1ba872" + "deferredHash": "844da7908a41692a3b04716c88e2f0cdad85ece6f94f6ab89fbd1ffe5c332fd2" } \ No newline at end of file diff --git a/tooling/v8-snapshot/cache/prod-win32/snapshot-meta.cache.json b/tooling/v8-snapshot/cache/prod-win32/snapshot-meta.cache.json index 7cc43329313e..834a847f8ad6 100644 --- a/tooling/v8-snapshot/cache/prod-win32/snapshot-meta.cache.json +++ b/tooling/v8-snapshot/cache/prod-win32/snapshot-meta.cache.json @@ -57,13 +57,15 @@ "./packages/server/lib/modes/record.js", "./packages/server/lib/modes/run.ts", "./packages/server/lib/open_project.ts", - "./packages/server/lib/util/process_profiler.ts", "./packages/server/lib/project-base.ts", "./packages/server/lib/socket-ct.ts", + "./packages/server/lib/util/process_profiler.ts", "./packages/server/lib/util/suppress_warnings.js", "./packages/server/node_modules/@benmalka/foxdriver/node_modules/graceful-fs/polyfills.js", + "./packages/server/node_modules/ci-info/index.js", "./packages/server/node_modules/glob/node_modules/minimatch/minimatch.js", "./packages/server/node_modules/graceful-fs/polyfills.js", + "./packages/server/node_modules/is-ci/index.js", "./packages/server/node_modules/mocha/node_modules/debug/src/node.js", "./packages/server/node_modules/signal-exit/index.js", "./process-nextick-args/index.js", @@ -937,7 +939,8 @@ "./packages/server/node_modules/uuid/dist/v3.js", "./packages/server/node_modules/uuid/dist/v4.js", "./packages/server/node_modules/uuid/dist/v5.js", - "./packages/server/server-entry.js", + "./packages/server/start-cypress.js", + "./packages/server/v8-snapshot-entry.js", "./packages/socket/index.js", "./packages/socket/lib/socket.ts", "./packages/socket/node_modules/socket.io/dist/broadcast-operator.js", @@ -3644,6 +3647,7 @@ "./packages/net-stubbing/node_modules/mime-types/index.js", "./packages/network/lib/allow-destroy.ts", "./packages/network/lib/blocked.ts", + "./packages/network/lib/ca.ts", "./packages/network/lib/concat-stream.ts", "./packages/network/lib/http-utils.ts", "./packages/network/lib/index.ts", @@ -3807,6 +3811,7 @@ "./packages/server/node_modules/@benmalka/foxdriver/node_modules/graceful-fs/legacy-streams.js", "./packages/server/node_modules/@benmalka/foxdriver/package.json", "./packages/server/node_modules/ansi-regex/index.js", + "./packages/server/node_modules/ci-info/vendors.json", "./packages/server/node_modules/cli-table3/index.js", "./packages/server/node_modules/cli-table3/src/cell.js", "./packages/server/node_modules/cli-table3/src/layout-manager.js", @@ -3931,5 +3936,5 @@ "./tooling/v8-snapshot/cache/prod-win32/snapshot-entry.js" ], "deferredHashFile": "yarn.lock", - "deferredHash": "5a515f98fe67a8a56a035563ff9c8b9e4a9edc4d035907c3fa5fef1bb60f1dfc" + "deferredHash": "7a05f23c5bcd4b5daed5b113c4a56ec620c8686ee7d956edecb5b117e41903bb" } \ No newline at end of file diff --git a/tooling/v8-snapshot/package.json b/tooling/v8-snapshot/package.json index 75def0874106..7bf3d70367ad 100644 --- a/tooling/v8-snapshot/package.json +++ b/tooling/v8-snapshot/package.json @@ -25,6 +25,7 @@ "resolve-from": "^5.0.0", "source-map-js": "^0.6.2", "temp-dir": "^2.0.0", + "terser": "5.12.1", "tslib": "^2.0.1", "worker-nodes": "^2.3.0" }, diff --git a/tooling/v8-snapshot/src/blueprint/set-globals.js b/tooling/v8-snapshot/src/blueprint/set-globals.js index 21dd4ed1b145..799b86b0f1cb 100644 --- a/tooling/v8-snapshot/src/blueprint/set-globals.js +++ b/tooling/v8-snapshot/src/blueprint/set-globals.js @@ -5,55 +5,71 @@ * Replaces globals that have been stubbed during snapshot creation with the * instances that are present in the app on startup. */ + // eslint-disable-next-line no-unused-vars -function setGlobals ( - newGlobal, - newProcess, - newWindow, - newDocument, - newConsole, - newPathResolver, - nodeRequire, -) { - // Populate the global function trampoline with the real global functions defined on newGlobal. - globalFunctionTrampoline = newGlobal - - for (let key of Object.keys(global)) { - newGlobal[key] = global[key] - } +(function () { + let numberOfSetGlobalsCalls = 0 + + return function setGlobals ( + newGlobal, + newProcess, + newWindow, + newDocument, + newConsole, + newPathResolver, + nodeRequire, + ) { + if (numberOfSetGlobalsCalls > 0) { + throw new Error('setGlobals should only be called once') + } - global = newGlobal + numberOfSetGlobalsCalls++ - if (typeof newProcess !== 'undefined') { - for (let key of Object.keys(process)) { - newProcess[key] = process[key] + // Populate the global function trampoline with the real global functions defined on newGlobal. + globalFunctionTrampoline = newGlobal + + for (let key of Object.keys(global)) { + newGlobal[key] = global[key] } - } - process = newProcess + global = newGlobal - if (typeof newWindow !== 'undefined') { - for (let key of Object.keys(window)) { - newWindow[key] = window[key] + if (typeof newProcess !== 'undefined') { + for (let key of Object.keys(process)) { + newProcess[key] = process[key] + } } - } - window = newWindow + process = newProcess - if (typeof newDocument !== 'undefined') { - for (let key of Object.keys(document)) { - newDocument[key] = document[key] + if (typeof newWindow !== 'undefined') { + for (let key of Object.keys(window)) { + newWindow[key] = window[key] + } } - } - document = newDocument + window = newWindow - for (let key of Object.keys(console)) { + if (typeof newDocument !== 'undefined') { + for (let key of Object.keys(document)) { + newDocument[key] = document[key] + } + } + + document = newDocument + + for (let key of Object.keys(console)) { // eslint-disable-next-line no-console - newConsole[key] = console[key] - } + newConsole[key] = console[key] + } + + console = newConsole + __pathResolver = newPathResolver + require = nodeRequire - console = newConsole - __pathResolver = newPathResolver - require = nodeRequire -} + if (typeof integrityCheck === 'function') { + // eslint-disable-next-line no-undef + integrityCheck({ require, pathResolver: __pathResolver }) + } + } +})() diff --git a/tooling/v8-snapshot/src/doctor/determine-deferred.ts b/tooling/v8-snapshot/src/doctor/determine-deferred.ts index 2b1393397f61..4a809de3e639 100644 --- a/tooling/v8-snapshot/src/doctor/determine-deferred.ts +++ b/tooling/v8-snapshot/src/doctor/determine-deferred.ts @@ -20,6 +20,7 @@ export async function determineDeferred ( forceNoRewrite: Set useHashBasedCache: boolean nodeEnv: string + integrityCheckSource: string | undefined }, ) { const jsonPath = path.join(cacheDir, 'snapshot-meta.json') @@ -73,6 +74,7 @@ export async function determineDeferred ( forceNoRewrite: opts.forceNoRewrite, nodeEnv: opts.nodeEnv, supportTypeScript: opts.nodeModulesOnly, + integrityCheckSource: opts.integrityCheckSource, }) const { diff --git a/tooling/v8-snapshot/src/doctor/process-script.worker.ts b/tooling/v8-snapshot/src/doctor/process-script.worker.ts index 4ba9e393758d..4c30c1e4167d 100644 --- a/tooling/v8-snapshot/src/doctor/process-script.worker.ts +++ b/tooling/v8-snapshot/src/doctor/process-script.worker.ts @@ -74,6 +74,7 @@ export function processScript ({ entryPoint, nodeEnv, supportTypeScript, + integrityCheckSource, }: ProcessScriptOpts): ProcessScriptResult { const bundleContent = getBundle(bundlePath, bundleHash) let snapshotScript @@ -86,6 +87,7 @@ export function processScript ({ baseSourcemapExternalPath: undefined, processedSourcemapExternalPath: undefined, supportTypeScript, + integrityCheckSource, }).script } catch (err: any) { return { outcome: 'failed:assembleScript', error: err } diff --git a/tooling/v8-snapshot/src/doctor/snapshot-doctor.ts b/tooling/v8-snapshot/src/doctor/snapshot-doctor.ts index ae3dfad427b6..03f620c447dd 100644 --- a/tooling/v8-snapshot/src/doctor/snapshot-doctor.ts +++ b/tooling/v8-snapshot/src/doctor/snapshot-doctor.ts @@ -266,6 +266,7 @@ export class SnapshotDoctor { private readonly nodeEnv: string private readonly _scriptProcessor: AsyncScriptProcessor private readonly _warningsProcessor: WarningsProcessor + private readonly integrityCheckSource: string | undefined /** * Creates an instance of the {@link SnapshotDoctor} @@ -284,6 +285,7 @@ export class SnapshotDoctor { this.previousNoRewrite = unpathify(opts.previousNoRewrite) this.forceNoRewrite = unpathify(opts.forceNoRewrite) this.nodeEnv = opts.nodeEnv + this.integrityCheckSource = opts.integrityCheckSource } /** @@ -499,6 +501,7 @@ export class SnapshotDoctor { entryPoint: `./${key}`, nodeEnv: this.nodeEnv, supportTypeScript: this.nodeModulesOnly, + integrityCheckSource: this.integrityCheckSource, }) assert(result != null, 'expected result from script processor') @@ -605,6 +608,7 @@ export class SnapshotDoctor { deferred: deferredArg, norewrite: norewriteArg, supportTypeScript: this.nodeModulesOnly, + integrityCheckSource: this.integrityCheckSource, }) return { warnings, meta: meta as Metadata, bundle } diff --git a/tooling/v8-snapshot/src/generator/blueprint.ts b/tooling/v8-snapshot/src/generator/blueprint.ts index 80f8cd6b7f28..89cc9eb347a5 100644 --- a/tooling/v8-snapshot/src/generator/blueprint.ts +++ b/tooling/v8-snapshot/src/generator/blueprint.ts @@ -51,6 +51,7 @@ export type BlueprintConfig = { sourceMap: Buffer | undefined processedSourceMapPath: string | undefined supportTypeScript: boolean + integrityCheckSource: string | undefined } const pathSep = path.sep === '\\' ? '\\\\' : path.sep @@ -101,6 +102,7 @@ export function scriptFromBlueprint (config: BlueprintConfig): { basedir, sourceMap, supportTypeScript, + integrityCheckSource, } = config const normalizedMainModuleRequirePath = forwardSlash(mainModuleRequirePath) @@ -110,6 +112,8 @@ export function scriptFromBlueprint (config: BlueprintConfig): { const PATH_SEP = '${pathSep}' var snapshotAuxiliaryData = ${auxiliaryData} +${integrityCheckSource || ''} + function generateSnapshot() { // // @@ -170,13 +174,32 @@ function generateSnapshot() { ${includeStrictVerifiers ? 'require.isStrict = true' : ''} customRequire(${normalizedMainModuleRequirePath}, ${normalizedMainModuleRequirePath}) - return { - customRequire, - setGlobals: ${setGlobals}, - } + const result = {} + Object.defineProperties(result, { + customRequire: { + writable: false, + value: customRequire + }, + setGlobals: { + writable: false, + value: ${setGlobals} + } + }) + return result } -var snapshotResult = generateSnapshot.call({}) +let numberOfGetSnapshotResultCalls = 0 +const snapshotResult = generateSnapshot.call({}) +Object.defineProperty(this, 'getSnapshotResult', { + writable: false, + value: function () { + if (numberOfGetSnapshotResultCalls > 0) { + throw new Error('getSnapshotResult can only be called once') + } + numberOfGetSnapshotResultCalls++ + return snapshotResult + }, +}) var supportTypeScript = ${supportTypeScript} generateSnapshot = null `, diff --git a/tooling/v8-snapshot/src/generator/create-snapshot-script.ts b/tooling/v8-snapshot/src/generator/create-snapshot-script.ts index 766b44a624f1..d16180667ceb 100644 --- a/tooling/v8-snapshot/src/generator/create-snapshot-script.ts +++ b/tooling/v8-snapshot/src/generator/create-snapshot-script.ts @@ -120,6 +120,7 @@ export function assembleScript ( resolverMap?: Record meta?: Metadata supportTypeScript: boolean + integrityCheckSource: string | undefined }, ): { script: Buffer, processedSourceMap?: string } { const includeStrictVerifiers = opts.includeStrictVerifiers ?? false @@ -179,6 +180,7 @@ export function assembleScript ( basedir, processedSourceMapPath: opts.processedSourcemapExternalPath, supportTypeScript: opts.supportTypeScript, + integrityCheckSource: opts.integrityCheckSource, } // 5. Finally return the rendered script buffer and optionally processed @@ -234,6 +236,7 @@ export async function createSnapshotScript ( resolverMap: opts.resolverMap, meta, supportTypeScript: opts.supportTypeScript, + integrityCheckSource: opts.integrityCheckSource, }, ) diff --git a/tooling/v8-snapshot/src/generator/snapshot-generate-entry-via-dependencies.ts b/tooling/v8-snapshot/src/generator/snapshot-generate-entry-via-dependencies.ts index eb2b92817af9..6db36953a109 100644 --- a/tooling/v8-snapshot/src/generator/snapshot-generate-entry-via-dependencies.ts +++ b/tooling/v8-snapshot/src/generator/snapshot-generate-entry-via-dependencies.ts @@ -20,6 +20,7 @@ class SnapshotEntryGeneratorViaWalk { readonly fullPathToSnapshotEntry: string, readonly nodeModulesOnly: boolean, readonly pathsMapper: PathsMapper, + readonly integrityCheckSource: string | undefined, ) { this.bundlerPath = getBundlerPath() } @@ -59,6 +60,7 @@ class SnapshotEntryGeneratorViaWalk { nodeModulesOnly: this.nodeModulesOnly, sourcemap: false, supportTypeScript: this.nodeModulesOnly, + integrityCheckSource: this.integrityCheckSource, } const { meta } = await createBundleAsync(opts) @@ -78,6 +80,7 @@ type GenerateDepsDataOpts = { entryFile: string nodeModulesOnly?: boolean pathsMapper?: PathsMapper + integrityCheckSource: string | undefined } export type BundlerMetadata = Metadata & { projectBaseDir: string } @@ -94,6 +97,7 @@ export async function generateBundlerMetadata ( fullPathToSnapshotEntry, fullConf.nodeModulesOnly, fullConf.pathsMapper, + config.integrityCheckSource, ) const meta = await generator.getMetadata() @@ -120,6 +124,7 @@ export async function generateSnapshotEntryFromEntryDependencies ( fullPathToSnapshotEntry, fullConf.nodeModulesOnly, fullConf.pathsMapper, + config.integrityCheckSource, ) try { diff --git a/tooling/v8-snapshot/src/generator/snapshot-generator.ts b/tooling/v8-snapshot/src/generator/snapshot-generator.ts index 43f01c22c383..ea96fb392453 100644 --- a/tooling/v8-snapshot/src/generator/snapshot-generator.ts +++ b/tooling/v8-snapshot/src/generator/snapshot-generator.ts @@ -100,6 +100,7 @@ export type GenerationOpts = { nodeEnv: string minify: boolean supportTypeScript: boolean + integrityCheckSource: string | undefined } function getDefaultGenerationOpts (projectBaseDir: string): GenerationOpts { @@ -114,6 +115,7 @@ function getDefaultGenerationOpts (projectBaseDir: string): GenerationOpts { nodeEnv: 'development', minify: false, supportTypeScript: false, + integrityCheckSource: undefined, } } @@ -156,6 +158,8 @@ export class SnapshotGenerator { private readonly nodeEnv: string /** See {@link GenerationOpts} minify */ private readonly minify: boolean + /** See {@link GenerationOpts} integrityCheckSource */ + private readonly integrityCheckSource: string | undefined /** * Path to the Go bundler binary used to generate the bundle with rewritten code * {@link https://github.com/cypress-io/esbuild/tree/thlorenz/snap} @@ -208,6 +212,7 @@ export class SnapshotGenerator { flags: mode, nodeEnv, minify, + integrityCheckSource, }: GenerationOpts = Object.assign( getDefaultGenerationOpts(projectBaseDir), opts, @@ -234,6 +239,7 @@ export class SnapshotGenerator { this._flags = new GeneratorFlags(mode) this.bundlerPath = getBundlerPath() this.minify = minify + this.integrityCheckSource = integrityCheckSource const auxiliaryDataKeys = Object.keys(this.auxiliaryData || {}) @@ -282,6 +288,7 @@ export class SnapshotGenerator { forceNoRewrite: this.forceNoRewrite, useHashBasedCache: this._flags.has(Flag.ReuseDoctorArtifacts), nodeEnv: this.nodeEnv, + integrityCheckSource: this.integrityCheckSource, }, )) } catch (err) { @@ -309,6 +316,7 @@ export class SnapshotGenerator { processedSourcemapExternalPath: this.snapshotScriptPath.replace('snapshot.js', 'processed.snapshot.js.map'), nodeEnv: this.nodeEnv, supportTypeScript: this.nodeModulesOnly, + integrityCheckSource: this.integrityCheckSource, }) } catch (err) { logError('Failed creating script') @@ -388,6 +396,7 @@ export class SnapshotGenerator { forceNoRewrite: this.forceNoRewrite, useHashBasedCache: this._flags.has(Flag.ReuseDoctorArtifacts), nodeEnv: this.nodeEnv, + integrityCheckSource: this.integrityCheckSource, }, )) } catch (err) { @@ -413,6 +422,7 @@ export class SnapshotGenerator { auxiliaryData: this.auxiliaryData, nodeEnv: this.nodeEnv, supportTypeScript: this.nodeModulesOnly, + integrityCheckSource: this.integrityCheckSource, }) } catch (err) { logError('Failed creating script') diff --git a/tooling/v8-snapshot/src/setup/config.ts b/tooling/v8-snapshot/src/setup/config.ts index 7d194723564f..ed6d04495bf3 100644 --- a/tooling/v8-snapshot/src/setup/config.ts +++ b/tooling/v8-snapshot/src/setup/config.ts @@ -13,6 +13,7 @@ type SnapshotConfig = { metaFile: string usePreviousSnapshotMetadata: boolean minify: boolean + integrityCheckSource: string | undefined } const platformString = process.platform @@ -20,7 +21,7 @@ const platformString = process.platform const snapshotCacheBaseDir = path.resolve(__dirname, '..', '..', 'cache') const projectBaseDir = path.join(__dirname, '..', '..', '..', '..') -const appEntryFile = require.resolve('@packages/server/server-entry.js') +const appEntryFile = require.resolve('@packages/server/v8-snapshot-entry') const cypressAppSnapshotDir = (cypressAppPath?: string) => { const electronPackageDir = path.join(projectBaseDir, 'packages', 'electron') @@ -83,14 +84,22 @@ const usePreviousSnapshotMetadata = process.env.V8_SNAPSHOT_FROM_SCRATCH == null * @param {string} env - 'dev' | 'prod' * @returns {SnapshotConfig} config to be used for all snapshot related tasks */ -export function createConfig (env: 'dev' | 'prod' = 'prod', cypressAppPath?: string): SnapshotConfig { +export function createConfig ({ + env = 'prod', + cypressAppPath, + integrityCheckSource, +}: { + env?: 'dev' | 'prod' + cypressAppPath?: string + integrityCheckSource: string | undefined +}): SnapshotConfig { /** * If true only node_module dependencies are included in the snapshot. Otherwise app files are included as well * * Configured via `env` */ const nodeModulesOnly = env === 'dev' - const minify = env === 'prod' + const minify = !process.env.V8_SNAPSHOT_DISABLE_MINIFY && env === 'prod' const snapshotCacheDir = env === 'dev' @@ -118,5 +127,6 @@ export function createConfig (env: 'dev' | 'prod' = 'prod', cypressAppPath?: str snapshotMetaPrevFile, usePreviousSnapshotMetadata, minify, + integrityCheckSource, } } diff --git a/tooling/v8-snapshot/src/setup/generate-entry.ts b/tooling/v8-snapshot/src/setup/generate-entry.ts index 50fba54cbbf7..6a16a879d677 100644 --- a/tooling/v8-snapshot/src/setup/generate-entry.ts +++ b/tooling/v8-snapshot/src/setup/generate-entry.ts @@ -18,12 +18,14 @@ export async function generateEntry ({ pathsMapper, projectBaseDir, snapshotEntryFile, + integrityCheckSource, }: { appEntryFile: string nodeModulesOnly: boolean pathsMapper: (file: string) => string projectBaseDir: string snapshotEntryFile: string + integrityCheckSource: string | undefined }): Promise { logInfo('Creating snapshot generation entry file %o', { nodeModulesOnly }) @@ -37,6 +39,7 @@ export async function generateEntry ({ entryFile: appEntryFile, pathsMapper, nodeModulesOnly, + integrityCheckSource, }, ) diff --git a/tooling/v8-snapshot/src/setup/generate-metadata.ts b/tooling/v8-snapshot/src/setup/generate-metadata.ts index 3b520028a0e3..55707f6b70d9 100644 --- a/tooling/v8-snapshot/src/setup/generate-metadata.ts +++ b/tooling/v8-snapshot/src/setup/generate-metadata.ts @@ -14,11 +14,13 @@ async function createMeta ({ pathsMapper, projectBaseDir, snapshotEntryFile, + integrityCheckSource, }) { return generateBundlerMetadata(projectBaseDir, snapshotEntryFile, { entryFile: appEntryFile, pathsMapper, nodeModulesOnly, + integrityCheckSource, }) } @@ -38,6 +40,7 @@ export async function generateMetadata ({ pathsMapper, projectBaseDir, snapshotEntryFile, + integrityCheckSource, }: { appEntryFile: string metaFile: string @@ -45,6 +48,7 @@ export async function generateMetadata ({ pathsMapper: (file: string) => string projectBaseDir: string snapshotEntryFile: string + integrityCheckSource: string | undefined }): Promise { try { logInfo('Creating snapshot metadata %o', { nodeModulesOnly }) @@ -55,6 +59,7 @@ export async function generateMetadata ({ pathsMapper, projectBaseDir, snapshotEntryFile, + integrityCheckSource, }) ensureDirSync(path.dirname(metaFile)) diff --git a/tooling/v8-snapshot/src/setup/index.ts b/tooling/v8-snapshot/src/setup/index.ts index e6745e6dfa26..4f6b0c39447d 100644 --- a/tooling/v8-snapshot/src/setup/index.ts +++ b/tooling/v8-snapshot/src/setup/index.ts @@ -6,10 +6,10 @@ import { generateEntry } from './generate-entry' import { installSnapshot } from './install-snapshot' import fs from 'fs-extra' -const setupV8Snapshots = async (baseCypressAppPath?: string) => { +const setupV8Snapshots = async ({ cypressAppPath, integrityCheckSource }: { cypressAppPath?: string, integrityCheckSource?: string} = {}) => { try { const args = minimist(process.argv.slice(2)) - const config = createConfig(args.env, baseCypressAppPath) + const config = createConfig({ env: args.env, cypressAppPath, integrityCheckSource }) await consolidateDeps(config) diff --git a/tooling/v8-snapshot/src/setup/install-snapshot.ts b/tooling/v8-snapshot/src/setup/install-snapshot.ts index abdbd34e3939..3c71d7bb3d90 100644 --- a/tooling/v8-snapshot/src/setup/install-snapshot.ts +++ b/tooling/v8-snapshot/src/setup/install-snapshot.ts @@ -35,6 +35,7 @@ function getSnapshotGenerator ({ usePreviousSnapshotMetadata, resolverMap, minify, + integrityCheckSource, }: { nodeModulesOnly: boolean projectBaseDir: string @@ -44,6 +45,7 @@ function getSnapshotGenerator ({ usePreviousSnapshotMetadata: boolean resolverMap: Record minify: boolean + integrityCheckSource: string | undefined }) { const { previousNoRewrite, @@ -66,6 +68,7 @@ function getSnapshotGenerator ({ resolverMap, forceNoRewrite, minify, + integrityCheckSource, }) } @@ -87,6 +90,7 @@ export async function installSnapshot ( snapshotMetaPrevFile, usePreviousSnapshotMetadata, minify, + integrityCheckSource, }, resolverMap, ) { @@ -105,6 +109,7 @@ export async function installSnapshot ( usePreviousSnapshotMetadata, resolverMap, minify, + integrityCheckSource, }) await snapshotGenerator.createScript() diff --git a/tooling/v8-snapshot/src/types.ts b/tooling/v8-snapshot/src/types.ts index 999ef2106e57..19f8d332a229 100644 --- a/tooling/v8-snapshot/src/types.ts +++ b/tooling/v8-snapshot/src/types.ts @@ -85,6 +85,7 @@ export type CreateBundleOpts = { baseSourcemapExternalPath?: string processedSourcemapExternalPath?: string supportTypeScript: boolean + integrityCheckSource: string | undefined } /** @@ -121,6 +122,8 @@ export type ProcessScriptOpts = { nodeEnv: string supportTypeScript: boolean + + integrityCheckSource: string | undefined } /** diff --git a/tooling/v8-snapshot/test/utils/bundle.ts b/tooling/v8-snapshot/test/utils/bundle.ts index b7a18eed6900..642dff6d7106 100644 --- a/tooling/v8-snapshot/test/utils/bundle.ts +++ b/tooling/v8-snapshot/test/utils/bundle.ts @@ -20,7 +20,7 @@ export function readSnapshotResult (cacheDir: string) { const sourcemapComment = snapshotFileContent.split('\n').pop() const { snapshotResult, snapshotAuxiliaryData } = eval( - `(function () {\n${snapshotFileContent};\n return { snapshotResult, snapshotAuxiliaryData };})()`, + `(function () {\n${snapshotFileContent};\n return { snapshotResult, snapshotAuxiliaryData };}).bind({})()`, ) return { meta, snapshotResult, snapshotAuxiliaryData, sourcemapComment } diff --git a/yarn.lock b/yarn.lock index da273db39e33..7326dbeab254 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11275,6 +11275,11 @@ byte-size@^5.0.1: resolved "https://registry.yarnpkg.com/byte-size/-/byte-size-5.0.1.tgz#4b651039a5ecd96767e71a3d7ed380e48bed4191" integrity sha512-/XuKeqWocKsYa/cBY1YbSJSWWqTi4cFgr9S6OyM7PBaPbr9zvNGwWP33vt0uqGhwDdN+y3yhbXVILEUpnwEWGw== +bytenode@1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/bytenode/-/bytenode-1.3.7.tgz#72b1426d08910ff0731099e4f40ee2929174ae34" + integrity sha512-TKvemYL2VJQIBE095FIYudjTsLagVBLpKXIYj+MaDUgzhdNL74SM1bizcXgwQs51mnXyO38tlqusDZqY8/XdTQ== + bytes@1: version "1.0.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-1.0.0.tgz#3569ede8ba34315fab99c3e92cb04c7220de1fa8" @@ -32429,16 +32434,7 @@ terser-webpack-plugin@^5.1.3: source-map "^0.6.1" terser "^5.7.2" -terser@^4.1.2, terser@^4.6.3: - version "4.8.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17" - integrity sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw== - dependencies: - commander "^2.20.0" - source-map "~0.6.1" - source-map-support "~0.5.12" - -terser@^5.10.0, terser@^5.7.2: +terser@5.12.1, terser@^5.10.0, terser@^5.7.2: version "5.12.1" resolved "https://registry.yarnpkg.com/terser/-/terser-5.12.1.tgz#4cf2ebed1f5bceef5c83b9f60104ac4a78b49e9c" integrity sha512-NXbs+7nisos5E+yXwAD+y7zrcTkMqb0dEJxIGtSKPdCBzopf7ni4odPul2aechpV7EXNvOudYOX2bb5tln1jbQ== @@ -32448,6 +32444,15 @@ terser@^5.10.0, terser@^5.7.2: source-map "~0.7.2" source-map-support "~0.5.20" +terser@^4.1.2, terser@^4.6.3: + version "4.8.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17" + integrity sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw== + dependencies: + commander "^2.20.0" + source-map "~0.6.1" + source-map-support "~0.5.12" + test-exclude@^5.2.3: version "5.2.3" resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-5.2.3.tgz#c3d3e1e311eb7ee405e092dac10aefd09091eac0"