Skip to content

Commit

Permalink
feat: Support dependencies in cy.origin() callback (#23283)
Browse files Browse the repository at this point in the history
Co-authored-by: Bill Glesias <bglesias@gmail.com>
  • Loading branch information
chrisbreiding and AtofStryker committed Sep 20, 2022
1 parent 6984a1a commit c48b80a
Show file tree
Hide file tree
Showing 37 changed files with 1,260 additions and 78 deletions.
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -901,7 +901,7 @@ commands:
fi
curl -L https://raw.githubusercontent.com/cypress-io/cypress/$branch/scripts/ensure-node.sh --output ci-ensure-node.sh
else
else
# if no .node-version file exists, we no-op the node script and use the global yarn
echo '' > ci-ensure-node.sh
fi
Expand Down
6 changes: 6 additions & 0 deletions cli/types/cypress.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,12 @@ declare namespace Cypress {
*/
off: Actions

/**
* Used to import dependencies within the cy.origin() callback
* @see https://on.cypress.io/origin
*/
require: (id: string) => any

/**
* Trigger action
* @private
Expand Down
17 changes: 0 additions & 17 deletions npm/webpack-preprocessor/deferred.ts

This file was deleted.

134 changes: 112 additions & 22 deletions npm/webpack-preprocessor/index.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
import Bluebird from 'bluebird'
import Debug from 'debug'
import _ from 'lodash'
import * as events from 'events'
import * as path from 'path'
import webpack from 'webpack'
import utils from './lib/utils'
import { crossOriginCallbackStore } from './lib/cross-origin-callback-store'
import { overrideSourceMaps } from './lib/typescript-overrides'
import { compileCrossOriginCallbackFiles } from './lib/cross-origin-callback-compile'

import * as Promise from 'bluebird'
import * as events from 'events'
import * as _ from 'lodash'
import * as webpack from 'webpack'
import { createDeferred } from './deferred'
const debug = Debug('cypress:webpack')
const debugStats = Debug('cypress:webpack:stats')

const path = require('path')
const debug = require('debug')('cypress:webpack')
const debugStats = require('debug')('cypress:webpack:stats')
declare global {
// this indicates which commands should be acted upon by the
// cross-origin-callback-loader. its absense means the loader should not
// be utilized at all
// eslint-disable-next-line no-var
var __cypressCallbackReplacementCommands: string[] | undefined
}

type FilePath = string
interface BundleObject {
promise: Promise<FilePath>
deferreds: Array<{ resolve: (filePath: string) => void, reject: (error: Error) => void, promise: Promise<string> }>
promise: Bluebird<FilePath>
deferreds: Array<{ resolve: (filePath: string) => void, reject: (error: Error) => void, promise: Bluebird<string> }>
initial: boolean
}

Expand Down Expand Up @@ -114,7 +124,7 @@ interface FileEvent extends events.EventEmitter {
* Cypress asks file preprocessor to bundle the given file
* and return the full path to produced bundle.
*/
type FilePreprocessor = (file: FileEvent) => Promise<FilePath>
type FilePreprocessor = (file: FileEvent) => Bluebird<FilePath>

type WebpackPreprocessorFn = (options: PreprocessorOptions) => FilePreprocessor

Expand Down Expand Up @@ -153,6 +163,8 @@ interface WebpackPreprocessor extends WebpackPreprocessorFn {
const preprocessor: WebpackPreprocessor = (options: PreprocessorOptions = {}): FilePreprocessor => {
debug('user options: %o', options)

let crossOriginCallbackLoaderAdded = false

// we return function that accepts the arguments provided by
// the event 'file:preprocessor'
//
Expand Down Expand Up @@ -229,6 +241,24 @@ const preprocessor: WebpackPreprocessor = (options: PreprocessorOptions = {}): F
})
.value() as any

const callbackReplacementCommands = global.__cypressCallbackReplacementCommands

if (!crossOriginCallbackLoaderAdded && !!callbackReplacementCommands) {
// webpack runs loaders last-to-first and we want ours to run last
// so that it's working with plain javascript
webpackOptions.module.rules.unshift({
test: /\.(js|ts|jsx|tsx)$/,
use: [{
loader: path.join(__dirname, 'lib/cross-origin-callback-loader'),
options: {
commands: callbackReplacementCommands,
},
}],
})

crossOriginCallbackLoaderAdded = true
}

debug('webpackOptions: %o', webpackOptions)
debug('watchOptions: %o', watchOptions)
if (options.typescript) debug('typescript: %s', options.typescript)
Expand All @@ -238,7 +268,7 @@ const preprocessor: WebpackPreprocessor = (options: PreprocessorOptions = {}): F

const compiler = webpack(webpackOptions)

let firstBundle = createDeferred<string>()
let firstBundle = utils.createDeferred<string>()

// cache the bundle promise, so it can be returned if this function
// is invoked again with the same filePath
Expand Down Expand Up @@ -301,29 +331,78 @@ const preprocessor: WebpackPreprocessor = (options: PreprocessorOptions = {}): F
console.error(stats.toString({ colors: true }))
}

// resolve with the outputPath so Cypress knows where to serve
// the file from
// Seems to be a race condition where changing file before next tick
const resolveAllBundles = () => {
bundles[filePath].deferreds.forEach((deferred) => {
// resolve with the outputPath so Cypress knows where to serve
// the file from
deferred.resolve(outputPath)
})

bundles[filePath].deferreds.length = 0
}

// the cross-origin-callback-loader extracts any cy.origin() callback
// functions that contains Cypress.require() and stores their sources
// in the CrossOriginCallbackStore. it saves the callbacks per source
// files, since that's the context it has. here we need to unfurl
// what dependencies the input source file has so we can know which
// files stored in the CrossOriginCallbackStore to compile
const handleCrossOriginCallbackFiles = () => {
// get the source file and any of its dependencies
const sourceFiles = jsonStats.modules
.filter((module) => {
// entries have duplicate modules whose ids are numbers
return _.isString(module.id)
})
.map((module) => {
// module id is the path relative to the cwd,
// e.g. ./cypress/support/e2e.js, but we need it absolute
return path.join(process.cwd(), module.id as string)
})

if (!crossOriginCallbackStore.hasFilesFor(sourceFiles)) {
debug('no cross-origin callback files')

return resolveAllBundles()
}

compileCrossOriginCallbackFiles(crossOriginCallbackStore.getFilesFor(sourceFiles), {
originalFilePath: filePath,
webpackOptions,
})
.then(() => {
debug('resolve all after handling cross-origin callback files')
resolveAllBundles()
})
.catch((err) => {
rejectWithErr(err)
})
.finally(() => {
crossOriginCallbackStore.reset(filePath)
})
}

// seems to be a race condition where changing file before next tick
// does not cause build to rerun
Promise.delay(0).then(() => {
Bluebird.delay(0).then(() => {
if (!bundles[filePath]) {
return
}

bundles[filePath].deferreds.forEach((deferred) => {
deferred.resolve(outputPath)
})
if (!callbackReplacementCommands) {
return resolveAllBundles()
}

bundles[filePath].deferreds.length = 0
handleCrossOriginCallbackFiles()
})
}

// this event is triggered when watching and a file is saved
const plugin = { name: 'CypressWebpackPreprocessor' }

// this event is triggered when watching and a file is saved
const onCompile = () => {
debug('compile', filePath)
const nextBundle = createDeferred<string>()
const nextBundle = utils.createDeferred<string>()

bundles[filePath].promise = nextBundle.promise
bundles[filePath].deferreds.push(nextBundle)
Expand Down Expand Up @@ -374,6 +453,17 @@ const preprocessor: WebpackPreprocessor = (options: PreprocessorOptions = {}): F
bundler.close(cb)
}
}

// clean up temp dir where cross-origin callback files are output
const tmpdir = utils.tmpdir(utils.hash(filePath))

debug('remove temp directory:', tmpdir)

utils.rmdir(tmpdir).catch((err) => {
// not the end of the world if removing the tmpdir fails, but we
// don't want it to crash the whole process by going uncaught
debug('failed removing temp directory: %s', err.stack)
})
})

// return the promise, which will resolve with the outputPath or reject
Expand Down
104 changes: 104 additions & 0 deletions npm/webpack-preprocessor/lib/cross-origin-callback-compile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import _ from 'lodash'
import Debug from 'debug'
import * as path from 'path'
import webpack from 'webpack'
import { CrossOriginCallbackStoreFile } from './cross-origin-callback-store'

const VirtualModulesPlugin = require('webpack-virtual-modules')

const debug = Debug('cypress:webpack')

interface Entry {
[key: string]: string
}

interface VirtualConfig {
[key: string]: string
}

interface EntryConfig {
entry: Entry
virtualConfig: VirtualConfig
}

// takes the files stored by the cross-origin-callback-loader and turns
// them into config we can pass to webpack to compile all the files. the
// virtual config allows us to just use the source we have in memory without
// needing to write it to file
const getConfig = ({ files, originalFilePath }): EntryConfig => {
const dir = path.dirname(originalFilePath)

return files.reduce((memo, file) => {
const { inputFileName, source } = file
const inputPath = path.join(dir, inputFileName)

memo.entry[inputFileName] = inputPath
memo.virtualConfig[inputPath] = source

return memo
}, { entry: {}, virtualConfig: {} })
}

interface ConfigProperties {
webpackOptions: webpack.Configuration
entry: Entry
virtualConfig: VirtualConfig
outputDir: string
}

const getWebpackOptions = ({ webpackOptions, entry, virtualConfig, outputDir }: ConfigProperties): webpack.Configuration => {
const modifiedWebpackOptions = _.extend({}, webpackOptions, {
entry,
output: {
path: outputDir,
},
})
const plugins = modifiedWebpackOptions.plugins || []

modifiedWebpackOptions.plugins = plugins.concat(
new VirtualModulesPlugin(virtualConfig),
)

return modifiedWebpackOptions
}

interface CompileOptions {
originalFilePath: string
webpackOptions: webpack.Configuration
}

// the cross-origin-callback-loader extracts any cy.origin() callback functions
// that contains Cypress.require() and stores their sources in the
// CrossOriginCallbackStore. this sends those sources through webpack again
// to process any dependencies and create bundles for each callback function
export const compileCrossOriginCallbackFiles = (files: CrossOriginCallbackStoreFile[], options: CompileOptions): Promise<void> => {
debug('compile cross-origin callback files: %o', files)

const { originalFilePath, webpackOptions } = options
const outputDir = path.dirname(files[0].outputFilePath)
const { entry, virtualConfig } = getConfig({ files, originalFilePath })
const modifiedWebpackOptions = getWebpackOptions({
webpackOptions,
entry,
virtualConfig,
outputDir,
})

return new Promise<void>((resolve, reject) => {
const compiler = webpack(modifiedWebpackOptions)

const handle = (err: Error) => {
if (err) {
debug('errored compiling cross-origin callback files with: %s', err.stack)

return reject(err)
}

debug('successfully compiled cross-origin callback files')

resolve()
}

compiler.run(handle)
})
}

4 comments on commit c48b80a

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on c48b80a Sep 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the linux arm64 version of the Test Runner.

Learn more about this pre-release platform-specific build at https://on.cypress.io/installing-cypress#Install-pre-release-version.

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/10.9.0/linux-arm64/develop-c48b80a0df14e9c22f17d1174372efd6a669b055/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on c48b80a Sep 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the darwin x64 version of the Test Runner.

Learn more about this pre-release platform-specific build at https://on.cypress.io/installing-cypress#Install-pre-release-version.

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/10.9.0/darwin-x64/develop-c48b80a0df14e9c22f17d1174372efd6a669b055/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on c48b80a Sep 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the darwin arm64 version of the Test Runner.

Learn more about this pre-release platform-specific build at https://on.cypress.io/installing-cypress#Install-pre-release-version.

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/10.9.0/darwin-arm64/develop-c48b80a0df14e9c22f17d1174372efd6a669b055/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on c48b80a Sep 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the win32 x64 version of the Test Runner.

Learn more about this pre-release platform-specific build at https://on.cypress.io/installing-cypress#Install-pre-release-version.

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/10.9.0/win32-x64/develop-c48b80a0df14e9c22f17d1174372efd6a669b055/cypress.tgz

Please sign in to comment.