feat: various v8 snapshot improvements (#24909)

Co-authored-by: Emily Rohrbough <emilyrohrbough@users.noreply.github.com>
This commit is contained in:
Ryan Manuel
2022-12-03 04:13:06 +00:00
committed by GitHub
parent 3d11bf81c9
commit 57b0eac60d
41 changed files with 658 additions and 158 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
# Bump this version to force CI to re-create the cache from scratch.
10-31-22
12-01-22
+6 -5
View File
@@ -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
+3 -1
View File
@@ -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",
+2 -7
View File
@@ -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')
+3 -3
View File
@@ -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)
+11 -10
View File
@@ -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()
+3 -2
View File
@@ -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",
+1
View File
@@ -0,0 +1 @@
require('./start-cypress')
+2 -2
View File
@@ -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
@@ -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
+10 -3
View File
@@ -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),
})
}
}
+59 -15
View File
@@ -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',
],
})
// 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(`copying server entry point bundle from ${path.join(workingDir, 'index.js')} to ${path.join(buildAppDir, 'packages', 'server', 'index.js')}`)
// 3. Remove all dependencies that are in the snapshot but not in the list of kept dependencies from the binary
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,
]
console.log(`potentially removing ${potentiallyRemovedDependencies.length} dependencies`)
// 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,
}
@@ -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)
}
@@ -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: '<embedded>',
},
{
fileName: '<embedded>',
},
{
functionName: 'snapshotRequire',
fileName: 'evalmachine.<anonymous>',
},
{
functionName: 'runWithSnapshot',
fileName: 'evalmachine.<anonymous>',
},
{
functionName: 'hookRequire',
fileName: 'evalmachine.<anonymous>',
},
{
functionName: 'startCypress',
fileName: 'evalmachine.<anonymous>',
},
{
fileName: 'evalmachine.<anonymous>',
},
{
functionName: 'Module._extensions.<computed>',
// 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',
})
}
+38
View File
@@ -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,
}
+19 -27
View File
@@ -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()
+91 -1
View File
@@ -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)
}
@@ -3542,5 +3542,5 @@
"./tooling/v8-snapshot/cache/dev-darwin/snapshot-entry.js"
],
"deferredHashFile": "yarn.lock",
"deferredHash": "8d8b606dabd3cc9e96c061293fa33f345f7bb2e66024b018d2edac2302b09f31"
"deferredHash": "844da7908a41692a3b04716c88e2f0cdad85ece6f94f6ab89fbd1ffe5c332fd2"
}
@@ -3541,5 +3541,5 @@
"./tooling/v8-snapshot/cache/dev-linux/snapshot-entry.js"
],
"deferredHashFile": "yarn.lock",
"deferredHash": "8d8b606dabd3cc9e96c061293fa33f345f7bb2e66024b018d2edac2302b09f31"
"deferredHash": "844da7908a41692a3b04716c88e2f0cdad85ece6f94f6ab89fbd1ffe5c332fd2"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
+1
View File
@@ -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"
},
@@ -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
(function () {
let numberOfSetGlobalsCalls = 0
for (let key of Object.keys(global)) {
newGlobal[key] = global[key]
}
global = newGlobal
if (typeof newProcess !== 'undefined') {
for (let key of Object.keys(process)) {
newProcess[key] = process[key]
return function setGlobals (
newGlobal,
newProcess,
newWindow,
newDocument,
newConsole,
newPathResolver,
nodeRequire,
) {
if (numberOfSetGlobalsCalls > 0) {
throw new Error('setGlobals should only be called once')
}
}
process = newProcess
numberOfSetGlobalsCalls++
if (typeof newWindow !== 'undefined') {
for (let key of Object.keys(window)) {
newWindow[key] = window[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]
}
}
window = newWindow
global = newGlobal
if (typeof newDocument !== 'undefined') {
for (let key of Object.keys(document)) {
newDocument[key] = document[key]
if (typeof newProcess !== 'undefined') {
for (let key of Object.keys(process)) {
newProcess[key] = process[key]
}
}
}
document = newDocument
process = newProcess
for (let key of Object.keys(console)) {
if (typeof newWindow !== 'undefined') {
for (let key of Object.keys(window)) {
newWindow[key] = window[key]
}
}
window = newWindow
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 })
}
}
})()
@@ -20,6 +20,7 @@ export async function determineDeferred (
forceNoRewrite: Set<string>
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 {
@@ -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 }
@@ -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 }
+28 -5
View File
@@ -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() {
//
// <process>
@@ -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
`,
@@ -120,6 +120,7 @@ export function assembleScript (
resolverMap?: Record<string, string>
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,
},
)
@@ -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 {
@@ -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')
+13 -3
View File
@@ -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,
}
}
@@ -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<void> {
logInfo('Creating snapshot generation entry file %o', { nodeModulesOnly })
@@ -37,6 +39,7 @@ export async function generateEntry ({
entryFile: appEntryFile,
pathsMapper,
nodeModulesOnly,
integrityCheckSource,
},
)
@@ -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<BundlerMetadata> {
try {
logInfo('Creating snapshot metadata %o', { nodeModulesOnly })
@@ -55,6 +59,7 @@ export async function generateMetadata ({
pathsMapper,
projectBaseDir,
snapshotEntryFile,
integrityCheckSource,
})
ensureDirSync(path.dirname(metaFile))
+2 -2
View File
@@ -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)
@@ -35,6 +35,7 @@ function getSnapshotGenerator ({
usePreviousSnapshotMetadata,
resolverMap,
minify,
integrityCheckSource,
}: {
nodeModulesOnly: boolean
projectBaseDir: string
@@ -44,6 +45,7 @@ function getSnapshotGenerator ({
usePreviousSnapshotMetadata: boolean
resolverMap: Record<string, string>
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()
+3
View File
@@ -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
}
/**
+1 -1
View File
@@ -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 }
+15 -10
View File
@@ -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"