mirror of
https://github.com/cypress-io/cypress.git
synced 2026-04-29 19:41:16 -05:00
feat: allow use of javascript in config file (#18061)
This commit is contained in:
committed by
GitHub
parent
20c8c5faca
commit
dedb05a0b5
@@ -490,7 +490,7 @@ const getMsgByType = function (type, arg1 = {}, arg2, arg3) {
|
||||
${chalk.yellow('Assign a different port with the \'--port <port>\' argument or shut down the other running process.')}`
|
||||
case 'ERROR_READING_FILE':
|
||||
filePath = `\`${arg1}\``
|
||||
err = `\`${arg2}\``
|
||||
err = `\`${arg2.type || arg2.code || arg2.name}: ${arg2.message}\``
|
||||
|
||||
return stripIndent`\
|
||||
Error reading from: ${chalk.blue(filePath)}
|
||||
@@ -1049,6 +1049,11 @@ const clone = function (err, options = {}) {
|
||||
|
||||
if (options.html) {
|
||||
obj.message = ansi_up.ansi_to_html(err.message)
|
||||
// revert back the distorted characters
|
||||
// in case there is an error in a child_process
|
||||
// that contains quotes
|
||||
.replace(/\&\#x27;/g, '\'')
|
||||
.replace(/\"\;/g, '"')
|
||||
} else {
|
||||
obj.message = err.message
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@ export class ProjectBase<TServer extends ServerE2E | ServerCt> extends EE {
|
||||
protected _server?: TServer
|
||||
protected _automation?: Automation
|
||||
private _recordTests?: any = null
|
||||
private _isServerOpen: boolean = false
|
||||
|
||||
public browser: any
|
||||
public options: OpenProjectLaunchOptions
|
||||
@@ -220,6 +221,8 @@ export class ProjectBase<TServer extends ServerE2E | ServerCt> extends EE {
|
||||
specsStore,
|
||||
})
|
||||
|
||||
this._isServerOpen = true
|
||||
|
||||
// if we didnt have a cfg.port
|
||||
// then get the port once we
|
||||
// open the server
|
||||
@@ -340,6 +343,10 @@ export class ProjectBase<TServer extends ServerE2E | ServerCt> extends EE {
|
||||
this.spec = null
|
||||
this.browser = null
|
||||
|
||||
if (!this._isServerOpen) {
|
||||
return
|
||||
}
|
||||
|
||||
const closePreprocessor = (this.testingType === 'e2e' && preprocessor.close) ?? undefined
|
||||
|
||||
await Promise.all([
|
||||
@@ -348,6 +355,8 @@ export class ProjectBase<TServer extends ServerE2E | ServerCt> extends EE {
|
||||
closePreprocessor?.(),
|
||||
])
|
||||
|
||||
this._isServerOpen = false
|
||||
|
||||
process.chdir(localCwd)
|
||||
|
||||
const config = this.getConfig()
|
||||
@@ -534,7 +543,7 @@ export class ProjectBase<TServer extends ServerE2E | ServerCt> extends EE {
|
||||
}
|
||||
|
||||
if (configFile !== false) {
|
||||
this.watchers.watch(settings.pathToConfigFile(projectRoot, { configFile }), obj)
|
||||
this.watchers.watchTree(settings.pathToConfigFile(projectRoot, { configFile }), obj)
|
||||
}
|
||||
|
||||
return this.watchers.watch(settings.pathToCypressEnvJson(projectRoot), obj)
|
||||
@@ -552,7 +561,7 @@ export class ProjectBase<TServer extends ServerE2E | ServerCt> extends EE {
|
||||
|
||||
try {
|
||||
Reporter.loadReporter(reporter, projectRoot)
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
const paths = Reporter.getSearchPathsForReporter(reporter, projectRoot)
|
||||
|
||||
// only include the message if this is the standard MODULE_NOT_FOUND
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import _ from 'lodash'
|
||||
import * as path from 'path'
|
||||
import * as cp from 'child_process'
|
||||
import * as inspector from 'inspector'
|
||||
import * as util from '../plugins/util'
|
||||
import * as errors from '../errors'
|
||||
import { fs } from '../util/fs'
|
||||
import Debug from 'debug'
|
||||
|
||||
const debug = Debug('cypress:server:require_async')
|
||||
|
||||
let requireProcess: cp.ChildProcess | null
|
||||
|
||||
interface RequireAsyncOptions{
|
||||
projectRoot: string
|
||||
loadErrorCode: string
|
||||
/**
|
||||
* members of the object returned that are functions and will need to be wrapped
|
||||
*/
|
||||
functionNames: string[]
|
||||
}
|
||||
|
||||
interface ChildOptions{
|
||||
stdio: 'inherit'
|
||||
execArgv?: string[]
|
||||
}
|
||||
|
||||
const killChildProcess = () => {
|
||||
requireProcess && requireProcess.kill()
|
||||
requireProcess = null
|
||||
}
|
||||
|
||||
export async function requireAsync (filePath: string, options: RequireAsyncOptions): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (requireProcess) {
|
||||
debug('kill existing config process')
|
||||
killChildProcess()
|
||||
}
|
||||
|
||||
if (/\.json$/.test(filePath)) {
|
||||
fs.readJson(path.resolve(options.projectRoot, filePath)).then((result) => resolve(result)).catch(reject)
|
||||
}
|
||||
|
||||
const childOptions: ChildOptions = {
|
||||
stdio: 'inherit',
|
||||
}
|
||||
|
||||
if (inspector.url()) {
|
||||
childOptions.execArgv = _.chain(process.execArgv.slice(0))
|
||||
.remove('--inspect-brk')
|
||||
.push(`--inspect=${process.debugPort + 1}`)
|
||||
.value()
|
||||
}
|
||||
|
||||
const childArguments = ['--projectRoot', options.projectRoot, '--file', filePath]
|
||||
|
||||
debug('fork child process', path.join(__dirname, 'require_async_child.js'), childArguments, childOptions)
|
||||
requireProcess = cp.fork(path.join(__dirname, 'require_async_child.js'), childArguments, childOptions)
|
||||
const ipc = util.wrapIpc(requireProcess)
|
||||
|
||||
if (requireProcess.stdout && requireProcess.stderr) {
|
||||
// manually pipe plugin stdout and stderr for dashboard capture
|
||||
// @see https://github.com/cypress-io/cypress/issues/7434
|
||||
requireProcess.stdout.on('data', (data) => process.stdout.write(data))
|
||||
requireProcess.stderr.on('data', (data) => process.stderr.write(data))
|
||||
}
|
||||
|
||||
ipc.on('loaded', (result) => {
|
||||
debug('resolving with result %o', result)
|
||||
resolve(result)
|
||||
})
|
||||
|
||||
ipc.on('load:error', (type, ...args) => {
|
||||
debug('load:error %s, rejecting', type)
|
||||
killChildProcess()
|
||||
|
||||
const err = errors.get(type, ...args)
|
||||
|
||||
// if it's a non-cypress error, restore the initial error
|
||||
if (!(err.message?.length)) {
|
||||
err.isCypressErr = false
|
||||
err.message = args[1]
|
||||
err.code = type
|
||||
err.name = type
|
||||
}
|
||||
|
||||
reject(err)
|
||||
})
|
||||
|
||||
debug('trigger the load of the file')
|
||||
ipc.send('load')
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
require('graceful-fs').gracefulify(require('fs'))
|
||||
const stripAnsi = require('strip-ansi')
|
||||
const debug = require('debug')('cypress:server:require_async:child')
|
||||
const util = require('../plugins/util')
|
||||
const ipc = util.wrapIpc(process)
|
||||
|
||||
require('./suppress_warnings').suppress()
|
||||
|
||||
const { file, projectRoot } = require('minimist')(process.argv.slice(2))
|
||||
|
||||
run(ipc, file, projectRoot)
|
||||
|
||||
/**
|
||||
* runs and returns the passed `requiredFile` file in the ipc `load` event
|
||||
* @param {*} ipc Inter Process Comunication protocol
|
||||
* @param {*} requiredFile the file we are trying to load
|
||||
* @param {*} projectRoot the root of the typescript project (useful mainly for tsnode)
|
||||
* @returns
|
||||
*/
|
||||
function run (ipc, requiredFile, projectRoot) {
|
||||
debug('requiredFile:', requiredFile)
|
||||
debug('projectRoot:', projectRoot)
|
||||
if (!projectRoot) {
|
||||
throw new Error('Unexpected: projectRoot should be a string')
|
||||
}
|
||||
|
||||
process.on('uncaughtException', (err) => {
|
||||
debug('uncaught exception:', util.serializeError(err))
|
||||
ipc.send('error', util.serializeError(err))
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
process.on('unhandledRejection', (event) => {
|
||||
const err = (event && event.reason) || event
|
||||
|
||||
debug('unhandled rejection:', util.serializeError(err))
|
||||
ipc.send('error', util.serializeError(err))
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
ipc.on('load', () => {
|
||||
try {
|
||||
debug('try loading', requiredFile)
|
||||
const exp = require(requiredFile)
|
||||
|
||||
const result = exp.default || exp
|
||||
|
||||
ipc.send('loaded', result)
|
||||
|
||||
debug('config %o', result)
|
||||
} catch (err) {
|
||||
if (err.name === 'TSError') {
|
||||
// beause of this https://github.com/TypeStrong/ts-node/issues/1418
|
||||
// we have to do this https://stackoverflow.com/questions/25245716/remove-all-ansi-colors-styles-from-strings/29497680
|
||||
const cleanMessage = stripAnsi(err.message)
|
||||
// replace the first line with better text (remove potentially misleading word TypeScript for example)
|
||||
.replace(/^.*\n/g, 'Error compiling file\n')
|
||||
|
||||
ipc.send('load:error', err.name, requiredFile, cleanMessage)
|
||||
} else {
|
||||
const realErrorCode = err.code || err.name
|
||||
|
||||
debug('failed to load file:%s\n%s: %s', requiredFile, realErrorCode, err.message)
|
||||
|
||||
ipc.send('load:error', realErrorCode, requiredFile, err.message)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -4,6 +4,19 @@ const path = require('path')
|
||||
const errors = require('../errors')
|
||||
const log = require('../log')
|
||||
const { fs } = require('../util/fs')
|
||||
const { requireAsync } = require('./require_async')
|
||||
const debug = require('debug')('cypress:server:settings')
|
||||
|
||||
function jsCode (obj) {
|
||||
const objJSON = obj && !_.isEmpty(obj)
|
||||
? JSON.stringify(_.omit(obj, 'configFile'), null, 2)
|
||||
: `{
|
||||
|
||||
}`
|
||||
|
||||
return `module.exports = ${objJSON}
|
||||
`
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// think about adding another PSemaphore
|
||||
@@ -78,7 +91,19 @@ module.exports = {
|
||||
},
|
||||
|
||||
_write (file, obj = {}) {
|
||||
return fs.outputJsonAsync(file, obj, { spaces: 2 })
|
||||
if (/\.json$/.test(file)) {
|
||||
debug('writing json file')
|
||||
|
||||
return fs.outputJsonAsync(file, obj, { spaces: 2 })
|
||||
.return(obj)
|
||||
.catch((err) => {
|
||||
return this._logWriteErr(file, err)
|
||||
})
|
||||
}
|
||||
|
||||
debug('writing javascript file')
|
||||
|
||||
return fs.writeFileAsync(file, jsCode(obj))
|
||||
.return(obj)
|
||||
.catch((err) => {
|
||||
return this._logWriteErr(file, err)
|
||||
@@ -110,7 +135,13 @@ module.exports = {
|
||||
return null
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Ensures the project at this root has a config file
|
||||
* that is readable and writable by the node process
|
||||
* @param {string} projectRoot root of the project
|
||||
* @param {object} options
|
||||
* @returns
|
||||
*/
|
||||
exists (projectRoot, options = {}) {
|
||||
const file = this.pathToConfigFile(projectRoot, options)
|
||||
|
||||
@@ -121,7 +152,7 @@ module.exports = {
|
||||
// directory is writable
|
||||
return fs.accessAsync(projectRoot, fs.W_OK)
|
||||
}).catch({ code: 'ENOENT' }, () => {
|
||||
// cypress.json does not exist, we missing project
|
||||
// cypress.json does not exist, completely new project
|
||||
log('cannot find file %s', file)
|
||||
|
||||
return this._err('CONFIG_FILE_NOT_FOUND', this.configFile(options), projectRoot)
|
||||
@@ -144,29 +175,45 @@ module.exports = {
|
||||
|
||||
const file = this.pathToConfigFile(projectRoot, options)
|
||||
|
||||
return fs.readJsonAsync(file)
|
||||
.catch({ code: 'ENOENT' }, () => {
|
||||
return this._write(file, {})
|
||||
}).then((json = {}) => {
|
||||
if (this.isComponentTesting(options) && 'component' in json) {
|
||||
json = { ...json, ...json.component }
|
||||
return requireAsync(file,
|
||||
{
|
||||
projectRoot,
|
||||
loadErrorCode: 'CONFIG_FILE_ERROR',
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.type === 'MODULE_NOT_FOUND' || err.code === 'ENOENT') {
|
||||
debug('file not found', file)
|
||||
|
||||
return this._write(file, {})
|
||||
}
|
||||
|
||||
if (!this.isComponentTesting(options) && 'e2e' in json) {
|
||||
json = { ...json, ...json.e2e }
|
||||
return Promise.reject(err)
|
||||
})
|
||||
.then((configObject = {}) => {
|
||||
if (this.isComponentTesting(options) && 'component' in configObject) {
|
||||
configObject = { ...configObject, ...configObject.component }
|
||||
}
|
||||
|
||||
const changed = this._applyRewriteRules(json)
|
||||
if (!this.isComponentTesting(options) && 'e2e' in configObject) {
|
||||
configObject = { ...configObject, ...configObject.e2e }
|
||||
}
|
||||
|
||||
debug('resolved configObject', configObject)
|
||||
const changed = this._applyRewriteRules(configObject)
|
||||
|
||||
// if our object is unchanged
|
||||
// then just return it
|
||||
if (_.isEqual(json, changed)) {
|
||||
return json
|
||||
if (_.isEqual(configObject, changed)) {
|
||||
return configObject
|
||||
}
|
||||
|
||||
// else write the new reduced obj
|
||||
return this._write(file, changed)
|
||||
.then((config) => {
|
||||
return config
|
||||
})
|
||||
}).catch((err) => {
|
||||
debug('an error occured when reading config', err)
|
||||
if (errors.isCypressErr(err)) {
|
||||
throw err
|
||||
}
|
||||
@@ -196,7 +243,7 @@ module.exports = {
|
||||
return Promise.resolve({})
|
||||
}
|
||||
|
||||
return this.read(projectRoot)
|
||||
return this.read(projectRoot, options)
|
||||
.then((settings) => {
|
||||
_.extend(settings, obj)
|
||||
|
||||
@@ -206,10 +253,6 @@ module.exports = {
|
||||
})
|
||||
},
|
||||
|
||||
remove (projectRoot, options = {}) {
|
||||
return fs.unlinkSync(this.pathToConfigFile(projectRoot, options))
|
||||
},
|
||||
|
||||
pathToConfigFile (projectRoot, options = {}) {
|
||||
const configFile = this.configFile(options)
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"chai": "1.10.0",
|
||||
"chalk": "2.4.2",
|
||||
"check-more-types": "2.24.0",
|
||||
"chokidar": "3.2.2",
|
||||
"chokidar": "3.5.1",
|
||||
"chrome-remote-interface": "0.28.2",
|
||||
"cli-table3": "0.5.1",
|
||||
"coffeescript": "1.12.7",
|
||||
|
||||
@@ -47,4 +47,11 @@ describe('e2e config', () => {
|
||||
project: Fixtures.projectPath('shadow-dom-global-inclusion'),
|
||||
})
|
||||
})
|
||||
|
||||
it('supports custom configFile in JavaScript', function () {
|
||||
return e2e.exec(this, {
|
||||
project: Fixtures.projectPath('config-with-custom-file-js'),
|
||||
configFile: 'cypress.config.custom.js',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -511,7 +511,11 @@ describe('lib/cypress', () => {
|
||||
return fs.statAsync(path.join(this.pristinePath, 'cypress', 'integration'))
|
||||
}).then(() => {
|
||||
throw new Error('integration folder should not exist!')
|
||||
}).catch({ code: 'ENOENT' }, () => {})
|
||||
}).catch((err) => {
|
||||
if (err.code !== 'ENOENT') {
|
||||
throw err
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('scaffolds out fixtures + files if they do not exist', function () {
|
||||
@@ -1795,29 +1799,26 @@ describe('lib/cypress', () => {
|
||||
})
|
||||
|
||||
it('reads config from a custom config file', function () {
|
||||
sinon.stub(fs, 'readJsonAsync')
|
||||
fs.readJsonAsync.withArgs(path.join(this.pristinePath, this.filename)).resolves({
|
||||
return fs.writeJson(path.join(this.pristinePath, this.filename), {
|
||||
env: { foo: 'bar' },
|
||||
port: 2020,
|
||||
})
|
||||
|
||||
fs.readJsonAsync.callThrough()
|
||||
|
||||
return cypress.start([
|
||||
`--config-file=${this.filename}`,
|
||||
])
|
||||
.then(() => {
|
||||
const options = Events.start.firstCall.args[0]
|
||||
|
||||
return Events.handleEvent(options, {}, {}, 123, 'open:project', this.pristinePath)
|
||||
}).then(() => {
|
||||
expect(this.open).to.be.called
|
||||
cypress.start([
|
||||
`--config-file=${this.filename}`,
|
||||
])
|
||||
.then(() => {
|
||||
const options = Events.start.firstCall.args[0]
|
||||
|
||||
const cfg = this.open.getCall(0).args[0]
|
||||
return Events.handleEvent(options, {}, {}, 123, 'open:project', this.pristinePath)
|
||||
}).then(() => {
|
||||
expect(this.open).to.be.called
|
||||
|
||||
expect(cfg.env.foo).to.equal('bar')
|
||||
const cfg = this.open.getCall(0).args[0]
|
||||
|
||||
expect(cfg.port).to.equal(2020)
|
||||
expect(cfg.env.foo).to.equal('bar')
|
||||
|
||||
expect(cfg.port).to.equal(2020)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
pageLoadTimeout: 10000,
|
||||
e2e: {
|
||||
defaultCommandTimeout: 500,
|
||||
videoCompression: 20,
|
||||
},
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
it('overrides config', () => {
|
||||
// overrides come from plugins
|
||||
expect(Cypress.config('defaultCommandTimeout')).to.eq(500)
|
||||
expect(Cypress.config('videoCompression')).to.eq(20)
|
||||
|
||||
// overrides come from CLI
|
||||
expect(Cypress.config('pageLoadTimeout')).to.eq(10000)
|
||||
})
|
||||
@@ -13,7 +13,7 @@ const cache = require(`${root}lib/cache`)
|
||||
const config = require(`${root}lib/config`)
|
||||
const scaffold = require(`${root}lib/scaffold`)
|
||||
const { ServerE2E } = require(`${root}lib/server-e2e`)
|
||||
const ProjectBase = require(`${root}lib/project-base`).ProjectBase
|
||||
const { ProjectBase } = require(`${root}lib/project-base`)
|
||||
const {
|
||||
getOrgs,
|
||||
paths,
|
||||
@@ -532,8 +532,10 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
|
||||
this.project = new ProjectBase({ projectRoot: '/_test-output/path/to/project-e2e', testingType: 'e2e' })
|
||||
|
||||
this.project._server = { close () {} }
|
||||
this.project._isServerOpen = true
|
||||
|
||||
sinon.stub(this.project, 'getConfig').returns(this.config)
|
||||
|
||||
sinon.stub(user, 'ensureAuthToken').resolves('auth-token-123')
|
||||
})
|
||||
|
||||
@@ -713,12 +715,14 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
|
||||
sinon.stub(settings, 'pathToConfigFile').returns('/path/to/cypress.json')
|
||||
sinon.stub(settings, 'pathToCypressEnvJson').returns('/path/to/cypress.env.json')
|
||||
this.watch = sinon.stub(this.project.watchers, 'watch')
|
||||
this.watchTree = sinon.stub(this.project.watchers, 'watchTree')
|
||||
})
|
||||
|
||||
it('watches cypress.json and cypress.env.json', function () {
|
||||
this.project.watchSettings({ onSettingsChanged () {} }, {})
|
||||
expect(this.watch).to.be.calledTwice
|
||||
expect(this.watch).to.be.calledWith('/path/to/cypress.json')
|
||||
expect(this.watch).to.be.calledOnce
|
||||
expect(this.watchTree).to.be.calledOnce
|
||||
expect(this.watchTree).to.be.calledWith('/path/to/cypress.json')
|
||||
|
||||
expect(this.watch).to.be.calledWith('/path/to/cypress.env.json')
|
||||
})
|
||||
|
||||
@@ -222,6 +222,10 @@ describe('lib/settings', () => {
|
||||
this.options = {
|
||||
configFile: 'my-test-config-file.json',
|
||||
}
|
||||
|
||||
this.optionsJs = {
|
||||
configFile: 'my-test-config-file.js',
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
@@ -254,5 +258,15 @@ describe('lib/settings', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('.read returns from configFile when its a JavaScript file', function () {
|
||||
return fs.writeFile(path.join(this.projectRoot, this.optionsJs.configFile), `module.exports = { baz: 'lurman' }`)
|
||||
.then(() => {
|
||||
return settings.read(this.projectRoot, this.optionsJs)
|
||||
.then((settings) => {
|
||||
expect(settings).to.deep.equal({ baz: 'lurman' })
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -13682,21 +13682,6 @@ chokidar-cli@2.1.0:
|
||||
lodash.throttle "^4.1.1"
|
||||
yargs "^13.3.0"
|
||||
|
||||
chokidar@3.2.2:
|
||||
version "3.2.2"
|
||||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.2.2.tgz#a433973350021e09f2b853a2287781022c0dc935"
|
||||
integrity sha512-bw3pm7kZ2Wa6+jQWYP/c7bAZy3i4GwiIiMO2EeRjrE48l8vBqC/WvFhSF0xyM8fQiPEGvwMY/5bqDG7sSEOuhg==
|
||||
dependencies:
|
||||
anymatch "~3.1.1"
|
||||
braces "~3.0.2"
|
||||
glob-parent "~5.1.0"
|
||||
is-binary-path "~2.1.0"
|
||||
is-glob "~4.0.1"
|
||||
normalize-path "~3.0.0"
|
||||
readdirp "~3.2.0"
|
||||
optionalDependencies:
|
||||
fsevents "~2.1.1"
|
||||
|
||||
chokidar@3.3.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.3.0.tgz#12c0714668c55800f659e262d4962a97faf554a6"
|
||||
|
||||
Reference in New Issue
Block a user