diff --git a/.eslintrc b/.eslintrc index b32d48eecb..ae9c088641 100644 --- a/.eslintrc +++ b/.eslintrc @@ -3,6 +3,7 @@ "plugin:cypress-dev/general" ], "env": { + "es6": true, "node": true } } diff --git a/packages/desktop-gui/cypress/fixtures/config.json b/packages/desktop-gui/cypress/fixtures/config.json index bc9e278b7c..066aaf2b28 100644 --- a/packages/desktop-gui/cypress/fixtures/config.json +++ b/packages/desktop-gui/cypress/fixtures/config.json @@ -27,9 +27,9 @@ "clientUrlDisplay": "http://localhost:2020", "commandTimeout": 4000, "cypressHostUrl": "http://localhost:2020", - "env": "development", - "environmentVariables": { - + "cypressEnv": "development", + "env": { + }, "execTimeout": 60000, "fileServerFolder": "/Users/jennifer/Dev/Projects/cypress-example-kitchensink", @@ -40,7 +40,7 @@ "isHeadless": false, "isNewProject": false, "javascripts": [ - + ], "morgan": true, "namespace": "__cypress", @@ -108,7 +108,7 @@ "from": "default", "value": 4000 }, - "environmentVariables": { + "env": { "fileServerFolder": { "from": "default", "value": "" diff --git a/packages/desktop-gui/cypress/integration/settings_spec.coffee b/packages/desktop-gui/cypress/integration/settings_spec.coffee index 74b7ffb655..e6be5e80da 100644 --- a/packages/desktop-gui/cypress/integration/settings_spec.coffee +++ b/packages/desktop-gui/cypress/integration/settings_spec.coffee @@ -65,7 +65,7 @@ describe "Settings", -> cy.contains("Your project's configuration is displayed") it "displays legend in table", -> - cy.get("table>tbody>tr").should("have.length", 5) + cy.get("table>tbody>tr").should("have.length", 6) it "wraps config line in proper classes", -> cy diff --git a/packages/desktop-gui/src/app/app.jsx b/packages/desktop-gui/src/app/app.jsx index 8c6bed32cb..4728f0ff17 100644 --- a/packages/desktop-gui/src/app/app.jsx +++ b/packages/desktop-gui/src/app/app.jsx @@ -20,7 +20,7 @@ class App extends Component { appApi.listenForMenuClicks() ipc.getOptions().then((options = {}) => { - appStore.set(_.pick(options, 'env', 'os', 'projectPath', 'version')) + appStore.set(_.pick(options, 'cypressEnv', 'os', 'projectPath', 'version')) viewStore.showApp() }) diff --git a/packages/desktop-gui/src/lib/app-store.js b/packages/desktop-gui/src/lib/app-store.js index 220e869a99..e243d9cf56 100644 --- a/packages/desktop-gui/src/lib/app-store.js +++ b/packages/desktop-gui/src/lib/app-store.js @@ -2,7 +2,7 @@ import { action, computed, observable } from 'mobx' import localData from '../lib/local-data' class AppStore { - @observable env + @observable cypressEnv @observable os @observable projectPath = null @observable newVersion @@ -15,7 +15,7 @@ class AppStore { } @computed get isDev () { - return this.env === 'development' + return this.cypressEnv === 'development' } @computed get isGlobalMode () { @@ -27,7 +27,7 @@ class AppStore { } @action set (props) { - if (props.env != null) this.env = props.env + if (props.cypressEnv != null) this.cypressEnv = props.cypressEnv if (props.os != null) this.os = props.os if (props.projectPath != null) this.projectPath = props.projectPath if (props.version != null) this.version = this.newVersion = props.version diff --git a/packages/desktop-gui/src/settings/configuration.jsx b/packages/desktop-gui/src/settings/configuration.jsx index 3eb0ac18d5..aca3955b25 100644 --- a/packages/desktop-gui/src/settings/configuration.jsx +++ b/packages/desktop-gui/src/settings/configuration.jsx @@ -102,6 +102,10 @@ const Configuration = observer(({ project }) => ( CLI set from CLI arguments + + plugin + set from plugin file +
diff --git a/packages/desktop-gui/src/settings/settings.scss b/packages/desktop-gui/src/settings/settings.scss
index 88014b298d..d350ddcf6b 100644
--- a/packages/desktop-gui/src/settings/settings.scss
+++ b/packages/desktop-gui/src/settings/settings.scss
@@ -53,7 +53,7 @@
     padding: 10px;
     font-family: $font-mono;
 
-    .envFile:hover, .env:hover, .config:hover, .cli:hover, .default:hover {
+    .envFile:hover, .env:hover, .config:hover, .cli:hover, .plugin:hover, .default:hover {
       border-bottom: 1px dotted #777;
     }
 
@@ -83,7 +83,7 @@
     }
   }
 
-  .envFile, .env, .config, .cli, .default {
+  .envFile, .env, .config, .cli, .plugin, .default {
     font-family: $font-mono;
 
     padding: 2px;
@@ -109,6 +109,11 @@
     color: #A21313;
   }
 
+  .plugin {
+    background-color: #f0e7fc;
+    color: #134aa2;
+  }
+
 }
 
 .settings-wrapper {
diff --git a/packages/driver/src/cypress.coffee b/packages/driver/src/cypress.coffee
index 3f3c4f37a2..1b333cf872 100644
--- a/packages/driver/src/cypress.coffee
+++ b/packages/driver/src/cypress.coffee
@@ -119,13 +119,13 @@ class $Cypress
       longStackTraces: config.isInteractive
     })
 
-    {environmentVariables, remote} = config
+    {env, remote} = config
 
-    config = _.omit(config, "environmentVariables", "remote")
+    config = _.omit(config, "env", "remote", "resolved", "scaffoldedFiles", "javascripts", "state")
 
     @state = $SetterGetter.create({})
     @config = $SetterGetter.create(config)
-    @env = $SetterGetter.create(environmentVariables)
+    @env = $SetterGetter.create(env)
 
     @Cookies = $Cookies.create(config.namespace, d)
 
diff --git a/packages/driver/src/cypress/error_messages.coffee b/packages/driver/src/cypress/error_messages.coffee
index ced55e6faa..bc9380a4ec 100644
--- a/packages/driver/src/cypress/error_messages.coffee
+++ b/packages/driver/src/cypress/error_messages.coffee
@@ -204,9 +204,6 @@ module.exports = {
     invalid_argument: "#{cmd('each')} must be passed a callback function."
     non_array: "#{cmd('each')} can only operate on an array like subject. Your subject was: '{{subject}}'"
 
-  env:
-    variables_missing: "Cypress.environmentVariables is not defined. Open an issue if you see this message."
-
   exec:
     failed: """#{cmd('exec', '\'{{cmd}}\'')} failed with the following error:
 
diff --git a/packages/driver/test/unit/cypress_spec.coffee b/packages/driver/test/unit/cypress_spec.coffee
index 36c0b209a5..4c401fba35 100644
--- a/packages/driver/test/unit/cypress_spec.coffee
+++ b/packages/driver/test/unit/cypress_spec.coffee
@@ -78,7 +78,7 @@ describe "src/cypress", ->
   describe "#env", ->
     beforeEach ->
       @Cypress.setConfig({
-        environmentVariables: {foo: "bar"}
+        env: {foo: "bar"}
       })
 
     it "acts as getter", ->
@@ -108,9 +108,9 @@ describe "src/cypress", ->
     it "instantiates Cookies", ->
       expect(@Cypress.Cookies).to.be.an("object")
 
-    it "passes config.environmentVariables", ->
+    it "passes config.env", ->
       @Cypress.setConfig({
-        environmentVariables: {foo: "bar"}
+        env: {foo: "bar"}
       })
 
       expect(@Cypress.env()).to.deep.eq({foo: "bar"})
diff --git a/packages/runner/src/app/app.jsx b/packages/runner/src/app/app.jsx
index 7f2e5be2f0..b4cf14e6e8 100644
--- a/packages/runner/src/app/app.jsx
+++ b/packages/runner/src/app/app.jsx
@@ -121,7 +121,7 @@ App.propTypes = {
       majorVersion: PropTypes.string.isRequired,
       version: PropTypes.string.isRequired,
     })).isRequired,
-    env: PropTypes.string.isRequired,
+    cypressEnv: PropTypes.string.isRequired,
     integrationFolder: PropTypes.string.isRequired,
     numTestsKeptInMemory: PropTypes.number.isRequired,
     projectName: PropTypes.string.isRequired,
diff --git a/packages/runner/src/app/app.spec.jsx b/packages/runner/src/app/app.spec.jsx
index 9db076d740..87617e535a 100644
--- a/packages/runner/src/app/app.spec.jsx
+++ b/packages/runner/src/app/app.spec.jsx
@@ -13,7 +13,7 @@ import App from './app'
 const createProps = () => ({
   config: {
     browsers: [],
-    env: 'tst',
+    cypressEnv: 'tst',
     integrationFolder: '',
     numTestsKeptInMemory: 1,
     projectName: '',
diff --git a/packages/runner/src/app/container.spec.jsx b/packages/runner/src/app/container.spec.jsx
index 213a49ba40..e95bb02071 100644
--- a/packages/runner/src/app/container.spec.jsx
+++ b/packages/runner/src/app/container.spec.jsx
@@ -14,7 +14,7 @@ import Container, { automationElementId } from './container'
 const createProps = () => ({
   config: {
     browsers: [],
-    env: 'test',
+    cypressEnv: 'test',
     integrationFolder: '',
     numTestsKeptInMemory: 1,
     projectName: '',
diff --git a/packages/runner/src/lib/event-manager.js b/packages/runner/src/lib/event-manager.js
index 4de6531546..fbe2cb0cae 100644
--- a/packages/runner/src/lib/event-manager.js
+++ b/packages/runner/src/lib/event-manager.js
@@ -62,7 +62,7 @@ const eventManager = {
     })
 
     _.each(socketRerunEvents, (event) => {
-      channel.on(event,  this._reRun.bind(this))
+      channel.on(event, this._reRun.bind(this))
     })
 
     reporterBus.on('runner:console:error', (testId) => {
@@ -165,31 +165,7 @@ const eventManager = {
   },
 
   setup (config, specPath) {
-    Cypress = $Cypress.create(
-      _.pick(config,
-        'isTextTerminal',
-        'numTestsKeptInMemory',
-        'waitForAnimations',
-        'animationDistanceThreshold',
-        'defaultCommandTimeout',
-        'pageLoadTimeout',
-        'requestTimeout',
-        'responseTimeout',
-        'environmentVariables',
-        'xhrUrl',
-        'baseUrl',
-        'viewportWidth',
-        'viewportHeight',
-        'execTimeout',
-        'screenshotOnHeadlessFailure',
-        'namespace',
-        'remote',
-        'version',
-        'fixturesFolder',
-        'platform',
-        'arch'
-      )
-    )
+    Cypress = $Cypress.create(config)
 
     // expose Cypress globally
     window.Cypress = Cypress
diff --git a/packages/server/__snapshots__/plugins_spec.coffee b/packages/server/__snapshots__/plugins_spec.coffee
index 75179c368d..2936c55b2c 100644
--- a/packages/server/__snapshots__/plugins_spec.coffee
+++ b/packages/server/__snapshots__/plugins_spec.coffee
@@ -3,7 +3,7 @@ Started video recording: /foo/bar/.projects/plugins-async-error/cypress/videos/a
 
   (Tests Starting)
 
-Error: The following error was thrown by a plugin. We've stopped running your tests because this likely interrupts behavior critical to them.
+Error: The following error was thrown by a plugin. We've stopped running your tests because a plugin crashed.
 
 Error: Async error from plugins file
     at stack trace line
@@ -50,15 +50,16 @@ Started video recording: /foo/bar/.projects/working-preprocessor/cypress/videos/
   (Tests Starting)
 
 
+  ✓ is another spec
   ✓ is another spec
 
-  1 passing
+  2 passing
 
 
   (Tests Finished)
 
-  - Tests:           1
-  - Passes:          1
+  - Tests:           2
+  - Passes:          2
   - Failures:        0
   - Pending:         0
   - Duration:        10 seconds
@@ -73,6 +74,40 @@ Started video recording: /foo/bar/.projects/working-preprocessor/cypress/videos/
   - Finished processing:  /foo/bar/.projects/working-preprocessor/cypress/videos/abc123.mp4 (0 seconds)
 
 
+  (All Done)
+
+`
+
+exports['e2e plugins can modify config from plugins 1'] = `
+Started video recording: /foo/bar/.projects/plugin-config/cypress/videos/abc123.mp4
+
+  (Tests Starting)
+
+
+  ✓ overrides config
+  ✓ overrides env
+
+  2 passing
+
+
+  (Tests Finished)
+
+  - Tests:           2
+  - Passes:          2
+  - Failures:        0
+  - Pending:         0
+  - Duration:        10 seconds
+  - Screenshots:     0
+  - Video Recorded:  true
+  - Cypress Version: 1.2.3
+
+
+  (Video)
+
+  - Started processing:   Compressing to 20 CRF
+  - Finished processing:  /foo/bar/.projects/plugin-config/cypress/videos/abc123.mp4 (0 seconds)
+
+
   (All Done)
 
 `
diff --git a/packages/server/lib/config.coffee b/packages/server/lib/config.coffee
index 3dc5e5fdd4..62b42b6556 100644
--- a/packages/server/lib/config.coffee
+++ b/packages/server/lib/config.coffee
@@ -2,6 +2,7 @@ _        = require("lodash")
 path     = require("path")
 Promise  = require("bluebird")
 fs       = require("fs-extra")
+deepDiff = require("return-deep-diff")
 errors   = require("./errors")
 scaffold = require("./scaffold")
 errors   = require("./errors")
@@ -12,11 +13,16 @@ v        = require("./util/validation")
 log      = require("debug")("cypress:server:config")
 pathHelpers = require("./util/path_helpers")
 
-## cypress following by _
+## cypress followed by _
 cypressEnvRe = /^(cypress_)/i
 dashesOrUnderscoresRe = /^(_-)+/
+oneOrMoreSpacesRe = /\s+/
 
-toWords = (str) -> str.trim().split(/\s+/)
+toWords = (str) ->
+  str.trim().split(oneOrMoreSpacesRe)
+
+isCypressEnvLike = (key) ->
+  cypressEnvRe.test(key) and key isnt "CYPRESS_ENV"
 
 folders = toWords """
   fileServerFolder   fixturesFolder   integrationFolder   screenshotsFolder
@@ -27,7 +33,7 @@ configKeys = toWords """
   animationDistanceThreshold      fileServerFolder
   baseUrl                         fixturesFolder
   chromeWebSecurity               integrationFolder
-  environmentVariables            pluginsFile
+  env                             pluginsFile
   hosts                           screenshotsFolder
   numTestsKeptInMemory            supportFile
   port                            supportFolder
@@ -45,16 +51,13 @@ configKeys = toWords """
   waitForAnimations
 """
 
-isCypressEnvLike = (key) ->
-  cypressEnvRe.test(key) and key isnt "CYPRESS_ENV"
-
 defaults = {
   port:                          null
   hosts:                         null
   morgan:                        true
   baseUrl:                       null
   socketId:                      null
-  isTextTerminal:                    false
+  isTextTerminal:                false
   reporter:                      "spec"
   reporterOptions:               null
   clientRoute:                   "/__/"
@@ -184,21 +187,25 @@ module.exports = {
 
     _.extend config, _.pick(options, "morgan", "isTextTerminal", "socketId", "report", "browsers")
 
-    _.each @whitelist(options), (val, key) ->
+    _
+    .chain(@whitelist(options))
+    .omit("env")
+    .each (val, key) ->
       resolved[key] = "cli"
       config[key] = val
       return
+    .value()
 
     if url = config.baseUrl
       ## always strip trailing slashes
       config.baseUrl = _.trimEnd(url, "/")
 
-    _.defaults config, defaults
+    _.defaults(config, defaults)
 
     ## split out our own app wide env from user env variables
     ## and delete envFile
-    config.environmentVariables = @parseEnv(config, resolved)
-    config.env = process.env["CYPRESS_ENV"]
+    config.env = @parseEnv(config, options.env, resolved)
+    config.cypressEnv = process.env["CYPRESS_ENV"]
     delete config.envFile
 
     ## when headless
@@ -230,6 +237,32 @@ module.exports = {
 
     return obj
 
+  updateWithPluginValues: (cfg, overrides = {}) ->
+    ## diff the overrides with cfg
+    ## including nested objects (env)
+    diffs = deepDiff(cfg, overrides, true)
+
+    setResolvedOn = (resolvedObj, obj) ->
+      _.each obj, (val, key) ->
+        if _.isObject(val)
+          ## recurse setting overrides
+          ## inside of this nested objected
+          setResolvedOn(resolvedObj[key], val)
+        else
+          ## override the resolved value
+          resolvedObj[key] = {
+            value: val
+            from: "plugin"
+          }
+
+    ## for each override go through
+    ## and change the resolved values of cfg
+    ## to point to the plugin
+    setResolvedOn(cfg.resolved, diffs)
+
+    ## merge cfg into overrides
+    _.defaultsDeep(diffs, cfg)
+
   resolveConfigValues: (config, defaults, resolved = {}) ->
     ## pick out only the keys found in configKeys
     _
@@ -418,8 +451,8 @@ module.exports = {
 
     return obj
 
-  parseEnv: (cfg, resolved = {}) ->
-    envVars = resolved.environmentVariables = {}
+  parseEnv: (cfg, envCLI, resolved = {}) ->
+    envVars = resolved.env = {}
 
     resolveFrom = (from, obj = {}) ->
       _.each obj, (val, key) ->
@@ -431,7 +464,7 @@ module.exports = {
     envCfg  = cfg.env ? {}
     envFile = cfg.envFile ? {}
     envProc = @getProcessEnvVars(process.env) ? {}
-    envCLI  = cfg.environmentVariables ? {}
+    envCLI  = envCLI ? {}
 
     matchesConfigKey = (key) ->
       if _.has(cfg, key)
diff --git a/packages/server/lib/errors.coffee b/packages/server/lib/errors.coffee
index 009a37b492..92cdc4965d 100644
--- a/packages/server/lib/errors.coffee
+++ b/packages/server/lib/errors.coffee
@@ -251,7 +251,7 @@ API = {
 
         Your pluginsFile is set to '#{arg1}', but either the file is missing, it contains a syntax error, or threw an error when required. The pluginsFile must be a .js or .coffee file.
 
-        Correct your cypress.json, create or fix the file, or set pluginsFile to false if a plugins file is not necessary for your project.
+        Please fix this, or set 'pluginsFile' to 'false' if a plugins file is not necessary for your project.
 
         #{if arg2 then "The following error was thrown:" else ""}
 
@@ -273,13 +273,13 @@ API = {
 
         We invoked the function exported by '#{arg1}', but it threw an error.
 
-        This is likely an error in the code of the plugins file itself.
+        The following error was thrown:
 
         #{chalk.yellow(arg2)}
         """.trim()
       when "PLUGINS_ERROR"
         """
-        The following error was thrown by a plugin. We've stopped running your tests because this likely interrupts behavior critical to them.
+        The following error was thrown by a plugin. We've stopped running your tests because a plugin crashed.
 
         #{chalk.yellow(arg1)}
         """.trim()
diff --git a/packages/server/lib/modes/headed.coffee b/packages/server/lib/modes/headed.coffee
index 3f0cca8147..df9833e178 100644
--- a/packages/server/lib/modes/headed.coffee
+++ b/packages/server/lib/modes/headed.coffee
@@ -88,7 +88,6 @@ module.exports = {
       Windows.open(@getWindowArgs(state, options))
       .then (win) =>
         Events.start(_.extend({}, options, {
-          env: process.env["CYPRESS_ENV"]
           onFocusTests: -> win.focus()
           os: os.platform()
         }), bus)
diff --git a/packages/server/lib/plugins/child/run_plugins.js b/packages/server/lib/plugins/child/run_plugins.js
index 332cc7888c..11fb7986b8 100644
--- a/packages/server/lib/plugins/child/run_plugins.js
+++ b/packages/server/lib/plugins/child/run_plugins.js
@@ -1,4 +1,5 @@
 const log = require('debug')('cypress:server:plugins:child')
+const Promise = require('bluebird')
 const preprocessor = require('./preprocessor')
 const util = require('../util')
 
@@ -20,7 +21,7 @@ const sendError = (ipc, err) => {
 
 let plugins
 
-const load = (ipc, config) => {
+const load = (ipc, config, pluginsFile) => {
   log('run plugins function')
 
   let callbackIdCount = 0
@@ -40,12 +41,16 @@ const load = (ipc, config) => {
     })
   }
 
-  try {
-    plugins(register, config)
-    ipc.send('loaded', registrations)
-  } catch (err) {
-    ipc.send('load:error', 'PLUGINS_FUNCTION_ERROR', util.serializeError(err))
-  }
+  Promise
+  .try(() => {
+    return plugins(register, config)
+  })
+  .then((modifiedCfg) => {
+    ipc.send('loaded', modifiedCfg, registrations)
+  })
+  .catch((err) => {
+    ipc.send('load:error', 'PLUGINS_FUNCTION_ERROR', pluginsFile, err.stack)
+  })
 }
 
 const execute = (ipc, event, ids, args = []) => {
@@ -93,7 +98,7 @@ module.exports = (ipc, pluginsFile) => {
   }
 
   ipc.on('load', (config) => {
-    load(ipc, config)
+    load(ipc, config, pluginsFile)
   })
 
   ipc.on('execute', (event, ids, args) => {
diff --git a/packages/server/lib/plugins/index.coffee b/packages/server/lib/plugins/index.coffee
index 92832b07cc..c8bba92db8 100644
--- a/packages/server/lib/plugins/index.coffee
+++ b/packages/server/lib/plugins/index.coffee
@@ -44,7 +44,7 @@ module.exports = {
 
       ipc.send("load", config)
 
-      ipc.on "loaded", (registrations) ->
+      ipc.on "loaded", (newCfg, registrations) ->
         _.each registrations, (registration) ->
           log("register plugins process event", registration.event, "with id", registration.callbackId)
           register registration.event, (args...) ->
@@ -55,7 +55,8 @@ module.exports = {
                 invocationId: invocationId
               }
               ipc.send("execute", registration.event, ids, args)
-        resolve()
+
+        resolve(newCfg)
 
       ipc.on "load:error", (type, args...) ->
         reject(errors.get(type, args...))
diff --git a/packages/server/lib/plugins/wrap_ipc.coffee b/packages/server/lib/plugins/wrap_ipc.coffee
deleted file mode 100644
index f55408aefe..0000000000
--- a/packages/server/lib/plugins/wrap_ipc.coffee
+++ /dev/null
@@ -1,20 +0,0 @@
-EE = require("events")
-
-module.exports = (aProcess) ->
-  emitter = new EE()
-
-  aProcess.on "message", (message) ->
-    emitter.emit(message.event, message.args...)
-
-  return {
-    send: (event, args...) ->
-      return if aProcess.killed
-
-      aProcess.send({
-        event: event
-        args
-      })
-
-    on: emitter.on.bind(emitter)
-    removeListener: emitter.removeListener.bind(emitter)
-  }
diff --git a/packages/server/lib/project.coffee b/packages/server/lib/project.coffee
index 3f77767e58..a23692f284 100644
--- a/packages/server/lib/project.coffee
+++ b/packages/server/lib/project.coffee
@@ -28,7 +28,7 @@ preprocessor = require("./plugins/preprocessor")
 settings    = require("./util/settings")
 browsers    = require("./browsers")
 scaffoldLog = require("debug")("cypress:server:scaffold")
-log         = require("debug")("cypress:server:project")
+debug       = require("debug")("cypress:server:project")
 
 fs   = Promise.promisifyAll(fs)
 glob = Promise.promisify(glob)
@@ -53,10 +53,10 @@ class Project extends EE
     @cfg         = null
     @memoryCheck = null
     @automation  = null
-    log("Project created %s", @projectRoot)
+    debug("Project created %s", @projectRoot)
 
   open: (options = {}) ->
-    log("opening project instance %s", @projectRoot)
+    debug("opening project instance %s", @projectRoot)
     @server = Server()
 
     _.defaults options, {
@@ -68,14 +68,27 @@ class Project extends EE
 
     if process.env.CYPRESS_MEMORY
       logMemory = ->
-        console.log("memory info", process.memoryUsage())
+        console.debug("memory info", process.memoryUsage())
 
       @memoryCheck = setInterval(logMemory, 1000)
 
     @getConfig(options)
-    .then (cfg) =>
+    .tap (cfg) =>
       process.chdir(@projectRoot)
 
+      ## TODO: we currently always scaffold the plugins file
+      ## even when headlessly or else it will cause an error when
+      ## we try to load it and it's not there. We must do this here
+      ## else initialing the plugins will instantly fail.
+      if cfg.pluginsFile
+        scaffold.plugins(path.dirname(cfg.pluginsFile), cfg)
+    .then (cfg) =>
+      @_initPlugins(cfg, options)
+      .then (modifiedCfg) ->
+        debug("plugin config yielded", modifiedCfg)
+
+        return config.updateWithPluginValues(cfg, modifiedCfg)
+    .then (cfg) =>
       @server.open(cfg, @)
       .spread (port, warning) =>
         ## if we didnt have a cfg.port
@@ -103,18 +116,24 @@ class Project extends EE
         )
         .then =>
           Promise.join(
-            @watchSupportFile(cfg)
+            @checkSupportFile(cfg)
             @watchPluginsFile(cfg, options)
           )
-        .then =>
-          @_initPlugins(cfg, options)
 
     # return our project instance
     .return(@)
 
-  _initPlugins: (config, options) ->
-    plugins.init(config, {
+  _initPlugins: (cfg, options) ->
+    ## only init plugins with the
+    ## whitelisted config values to
+    ## prevent tampering with the
+    ## internals and breaking cypress
+    cfg = config.whitelist(cfg)
+
+    plugins.init(cfg, {
       onError: (err) ->
+        debug('got plugins error', err.stack)
+
         browsers.close()
         options.onError(err)
     })
@@ -128,13 +147,13 @@ class Project extends EE
       api.getProjectRuns(projectId, authToken)
 
   reset: ->
-    log("resetting project instance %s", @projectRoot)
+    debug("resetting project instance %s", @projectRoot)
 
     Promise.try =>
       @server?.reset()
 
   close: ->
-    log("closing project instance %s", @projectRoot)
+    debug("closing project instance %s", @projectRoot)
 
     if @memoryCheck
       clearInterval(@memoryCheck)
@@ -149,43 +168,31 @@ class Project extends EE
     .then ->
       process.chdir(localCwd)
 
-  watchSupportFile: (config) ->
-    if supportFile = config.supportFile
+  checkSupportFile: (cfg) ->
+    if supportFile = cfg.supportFile
       fs.pathExists(supportFile)
       .then (found) =>
         if not found
           errors.throw("SUPPORT_FILE_NOT_FOUND", supportFile)
 
-        relativePath = path.relative(config.projectRoot, config.supportFile)
-        if config.watchForFileChanges isnt false
-          options = {
-            onChange: _.bind(@server.onTestFileChange, @server, relativePath)
-          }
-        preprocessor.getFile(relativePath, config, options)
-        ## ignore errors b/c we're just setting up the watching. errors
-        ## are handled by the spec controller
-        .catch ->
-    else
-      Promise.resolve()
-
-  watchPluginsFile: (config, options) ->
-    log("attempt watch plugins file: #{config.pluginsFile}")
-    if not config.pluginsFile
+  watchPluginsFile: (cfg, options) ->
+    debug("attempt watch plugins file: #{cfg.pluginsFile}")
+    if not cfg.pluginsFile
       return Promise.resolve()
 
-    fs.pathExists(config.pluginsFile)
+    fs.pathExists(cfg.pluginsFile)
     .then (found) =>
-      log("plugins file found? #{found}")
+      debug("plugins file found? #{found}")
       ## ignore if not found. plugins#init will throw the right error
       return if not found
 
-      log("watch plugins file")
-      @watchers.watch(config.pluginsFile, {
+      debug("watch plugins file")
+      @watchers.watch(cfg.pluginsFile, {
         onChange: =>
           ## TODO: completely re-open project instead?
-          log("plugins file changed")
+          debug("plugins file changed")
           ## re-init plugins after a change
-          @_initPlugins(config, options)
+          @_initPlugins(cfg, options)
           .catch (err) ->
             options.onError(err)
       })
@@ -209,21 +216,21 @@ class Project extends EE
 
     @watchers.watch(settings.pathToCypressJson(@projectRoot), obj)
 
-  watchSettingsAndStartWebsockets: (options = {}, config = {}) ->
+  watchSettingsAndStartWebsockets: (options = {}, cfg = {}) ->
     @watchSettings(options.onSettingsChanged)
 
     ## if we've passed down reporter
     ## then record these via mocha reporter
-    if config.report
-      if not Reporter.isValidReporterName(config.reporter, config.projectRoot)
-        paths = Reporter.getSearchPathsForReporter(config.reporter, config.projectRoot)
-        errors.throw("INVALID_REPORTER_NAME", config.reporter, paths)
+    if cfg.report
+      if not Reporter.isValidReporterName(cfg.reporter, cfg.projectRoot)
+        paths = Reporter.getSearchPathsForReporter(cfg.reporter, cfg.projectRoot)
+        errors.throw("INVALID_REPORTER_NAME", cfg.reporter, paths)
 
-      reporter = Reporter.create(config.reporter, config.reporterOptions, config.projectRoot)
+      reporter = Reporter.create(cfg.reporter, cfg.reporterOptions, cfg.projectRoot)
 
-    @automation = Automation.create(config.namespace, config.socketIoCookie, config.screenshotsFolder)
+    @automation = Automation.create(cfg.namespace, cfg.socketIoCookie, cfg.screenshotsFolder)
 
-    @server.startWebsockets(@automation, config, {
+    @server.startWebsockets(@automation, cfg, {
       onReloadBrowser: options.onReloadBrowser
 
       onFocusTests: options.onFocusTests
@@ -236,12 +243,12 @@ class Project extends EE
         @emit("socket:connected", id)
 
       onSetRunnables: (runnables) ->
-        log("onSetRunnables")
-        log("runnables", runnables)
+        debug("onSetRunnables")
+        debug("runnables", runnables)
         reporter?.setRunnables(runnables)
 
       onMocha: (event, runnable) =>
-        log("onMocha", event)
+        debug("onMocha", event)
         ## bail if we dont have a
         ## reporter instance
         return if not reporter
@@ -365,8 +372,8 @@ class Project extends EE
 
     [browserUrl, "#/tests", specUrl].join("/").replace(multipleForwardSlashesRe, replacer)
 
-  scaffold: (config) ->
-    log("scaffolding project %s", @projectRoot)
+  scaffold: (cfg) ->
+    debug("scaffolding project %s", @projectRoot)
 
     scaffolds = []
 
@@ -380,19 +387,13 @@ class Project extends EE
     ##
     ## ensure support dir is created
     ## and example support file if dir doesnt exist
-    push(scaffold.support(config.supportFolder, config))
-
-    ## TODO: we currently always scaffold the plugins file
-    ## even when headlessly or else it will cause an error when
-    ## we try to load it and it's not there
-    if config.pluginsFile
-      push(scaffold.plugins(path.dirname(config.pluginsFile), config))
+    push(scaffold.support(cfg.supportFolder, cfg))
 
     ## if we're in headed mode add these other scaffolding
     ## tasks
-    if not config.isTextTerminal
-      push(scaffold.integration(config.integrationFolder, config))
-      push(scaffold.fixture(config.fixturesFolder, config))
+    if not cfg.isTextTerminal
+      push(scaffold.integration(cfg.integrationFolder, cfg))
+      push(scaffold.fixture(cfg.fixturesFolder, cfg))
 
     Promise.all(scaffolds)
 
@@ -472,13 +473,13 @@ class Project extends EE
     _.extend({}, clientProject, {state: state})
 
   @_getProject = (clientProject, authToken) ->
-    log("get project from api", clientProject.id, clientProject.path)
+    debug("get project from api", clientProject.id, clientProject.path)
     api.getProject(clientProject.id, authToken)
     .then (project) ->
-      log("got project from api")
+      debug("got project from api")
       Project._mergeDetails(clientProject, project)
     .catch (err) ->
-      log("failed to get project from api", err.statusCode)
+      debug("failed to get project from api", err.statusCode)
       switch err.statusCode
         when 404
           ## project doesn't exist
@@ -490,40 +491,40 @@ class Project extends EE
           throw err
 
   @getProjectStatuses = (clientProjects = []) ->
-    log("get project statuses for #{clientProjects.length} projects")
+    debug("get project statuses for #{clientProjects.length} projects")
     user.ensureAuthToken()
     .then (authToken) ->
-      log("got auth token #{authToken}")
+      debug("got auth token #{authToken}")
       api.getProjects(authToken).then (projects = []) ->
-        log("got #{projects.length} projects")
+        debug("got #{projects.length} projects")
         projectsIndex = _.keyBy(projects, "id")
         Promise.all(_.map clientProjects, (clientProject) ->
-          log("looking at", clientProject.path)
+          debug("looking at", clientProject.path)
           ## not a CI project, just mark as valid and return
           if not clientProject.id
-            log("no project id")
+            debug("no project id")
             return Project._mergeState(clientProject, "VALID")
 
           if project = projectsIndex[clientProject.id]
-            log("found matching:", project)
+            debug("found matching:", project)
             ## merge in details for matching project
             Project._mergeDetails(clientProject, project)
           else
-            log("did not find matching:", project)
+            debug("did not find matching:", project)
             ## project has id, but no matching project found
             ## check if it doesn't exist or if user isn't authorized
             Project._getProject(clientProject, authToken)
         )
 
   @getProjectStatus = (clientProject) ->
-    log("get project status for", clientProject.id, clientProject.path)
+    debug("get project status for", clientProject.id, clientProject.path)
 
     if not clientProject.id
-      log("no project id")
+      debug("no project id")
       return Promise.resolve(Project._mergeState(clientProject, "VALID"))
 
     user.ensureAuthToken().then (authToken) ->
-      log("got auth token #{authToken}")
+      debug("got auth token #{authToken}")
       Project._getProject(clientProject, authToken)
 
   @remove = (path) ->
@@ -579,7 +580,7 @@ class Project extends EE
   # Given a path to the project, finds all specs
   # returns list of specs with respect to the project root
   @findSpecs = (projectPath, specPattern) ->
-    log("finding specs for project %s", projectPath)
+    debug("finding specs for project %s", projectPath)
     la(check.unemptyString(projectPath), "missing project path", projectPath)
     la(check.maybe.unemptyString(specPattern), "invalid spec pattern", specPattern)
 
diff --git a/packages/server/lib/server.coffee b/packages/server/lib/server.coffee
index a74e6d5725..c68f896968 100644
--- a/packages/server/lib/server.coffee
+++ b/packages/server/lib/server.coffee
@@ -439,7 +439,7 @@ class Server
     if fullyQualifiedUrl is "" or not fullyQualifiedRe.test(fullyQualifiedUrl)
       @_remoteOrigin = "http://#{DEFAULT_DOMAIN_NAME}:#{@_port()}"
       @_remoteStrategy = "file"
-      @_remoteFileServer = "http://#{DEFAULT_DOMAIN_NAME}:#{@_fileServer.port()}"
+      @_remoteFileServer = "http://#{DEFAULT_DOMAIN_NAME}:#{@_fileServer?.port()}"
       @_remoteDomainName = DEFAULT_DOMAIN_NAME
       @_remoteProps = null
 
diff --git a/packages/server/lib/util/args.coffee b/packages/server/lib/util/args.coffee
index 8e8cb25342..10aacdb0ca 100644
--- a/packages/server/lib/util/args.coffee
+++ b/packages/server/lib/util/args.coffee
@@ -75,7 +75,6 @@ module.exports = {
         "run-project": "runProject"
         "return-pkg":  "returnPkg"
         "auto-open":   "autoOpen"
-        "env":         "environmentVariables"
         "headless":    "isTextTerminal"
         "exit-with-code":   "exitWithCode"
         "reporter-options": "reporterOptions"
@@ -89,7 +88,6 @@ module.exports = {
     options = _
     .chain(options)
     .defaults(whitelisted)
-    .extend({env: process.env["CYPRESS_ENV"]})
     .mapValues(coerce)
     .value()
 
@@ -107,9 +105,9 @@ module.exports = {
       backup("hosts", options)
       options.hosts = parseNestedValues(hosts)
 
-    if envs = options.environmentVariables
-      backup("environmentVariables", options)
-      options.environmentVariables = parseNestedValues(envs)
+    if envs = options.env
+      backup("env", options)
+      options.env = parseNestedValues(envs)
 
     if ro = options.reporterOptions
       backup("reporterOptions", options)
diff --git a/packages/server/package.json b/packages/server/package.json
index ebb9e46f34..c742fc999d 100644
--- a/packages/server/package.json
+++ b/packages/server/package.json
@@ -138,6 +138,7 @@
     "randomstring": "^1.1.5",
     "request": "2.79.0",
     "request-promise": "4.1.1",
+    "return-deep-diff": "^0.2.9",
     "sanitize-filename": "^1.6.1",
     "semver": "^5.3.0",
     "send": "^0.14.1",
diff --git a/packages/server/test/e2e/plugins_spec.coffee b/packages/server/test/e2e/plugins_spec.coffee
index 25b328e26a..24c5098691 100644
--- a/packages/server/test/e2e/plugins_spec.coffee
+++ b/packages/server/test/e2e/plugins_spec.coffee
@@ -1,11 +1,12 @@
 e2e      = require("../support/helpers/e2e")
 Fixtures = require("../support/helpers/fixtures")
 
+pluginConfig = Fixtures.projectPath("plugin-config")
 workingPreprocessor = Fixtures.projectPath("working-preprocessor")
 pluginsAsyncError = Fixtures.projectPath("plugins-async-error")
 
 describe "e2e plugins", ->
-  e2e.setup({npmInstall: true})
+  e2e.setup()
 
   it "passes", ->
     e2e.exec(@, {
@@ -22,3 +23,13 @@ describe "e2e plugins", ->
       snapshot: true
       expectedExitCode: 1
     })
+
+  it "can modify config from plugins", ->
+    e2e.exec(@, {
+      spec: "app_spec.coffee"
+      env: "foo=foo,bar=bar"
+      config: "pageLoadTimeout=10000"
+      project: pluginConfig
+      snapshot: true
+      expectedExitCode: 0
+    })
diff --git a/packages/server/test/integration/cypress_spec.coffee b/packages/server/test/integration/cypress_spec.coffee
index edec4dfa65..75d27a3200 100644
--- a/packages/server/test/integration/cypress_spec.coffee
+++ b/packages/server/test/integration/cypress_spec.coffee
@@ -77,6 +77,7 @@ describe "lib/cypress", ->
     @todosPath    = Fixtures.projectPath("todos")
     @pristinePath = Fixtures.projectPath("pristine")
     @noScaffolding = Fixtures.projectPath("no-scaffolding")
+    @pluginConfig = Fixtures.projectPath("plugin-config")
     @idsPath      = Fixtures.projectPath("ids")
 
     ## force cypress to call directly into main without
@@ -630,6 +631,40 @@ describe "lib/cypress", ->
 
           @expectExitWith(0)
 
+      it "can override values in plugins", ->
+        cypress.start([
+          "--run-project=#{@pluginConfig}", "--config=requestTimeout=1234,videoCompression=false"
+          "--env=foo=foo,bar=bar"
+        ])
+        .then =>
+          cfg = openProject.getProject().cfg
+
+          expect(cfg.videoCompression).to.eq(20)
+          expect(cfg.defaultCommandTimeout).to.eq(500)
+          expect(cfg.env).to.deep.eq({
+            foo: "bar"
+            bar: "bar"
+          })
+
+          expect(cfg.resolved.videoCompression).to.deep.eq({
+            value: 20
+            from: "plugin"
+          })
+          expect(cfg.resolved.requestTimeout).to.deep.eq({
+            value: 1234
+            from: "cli"
+          })
+          expect(cfg.resolved.env.foo).to.deep.eq({
+            value: "bar"
+            from: "plugin"
+          })
+          expect(cfg.resolved.env.bar).to.deep.eq({
+            value: "bar"
+            from: "cli"
+          })
+
+          @expectExitWith(0)
+
     describe "--port", ->
       beforeEach ->
         headless.listenForProjectEnd.resolves({failures: 0})
@@ -675,7 +710,7 @@ describe "lib/cypress", ->
           "version=0.12.1,foo=bar,host=http://localhost:8888,baz=quux=dolor"
         ])
         .then =>
-          expect(openProject.getProject().cfg.environmentVariables).to.deep.eq({
+          expect(openProject.getProject().cfg.env).to.deep.eq({
             version: "0.12.1"
             foo: "bar"
             host: "http://localhost:8888"
@@ -1070,7 +1105,7 @@ describe "lib/cypress", ->
           port: 2121
           pageLoadTimeout: 1000
           report: false
-          environmentVariables: { baz: "baz" }
+          env: { baz: "baz" }
         })
 
         cfg = open.getCall(0).args[0]
@@ -1081,12 +1116,12 @@ describe "lib/cypress", ->
         expect(cfg.baseUrl).to.eq("localhost")
         expect(cfg.watchForFileChanges).to.be.false
         expect(cfg.responseTimeout).to.eq(5555)
-        expect(cfg.environmentVariables.baz).to.eq("baz")
-        expect(cfg.environmentVariables).not.to.have.property("fileServerFolder")
-        expect(cfg.environmentVariables).not.to.have.property("port")
-        expect(cfg.environmentVariables).not.to.have.property("BASE_URL")
-        expect(cfg.environmentVariables).not.to.have.property("watchForFileChanges")
-        expect(cfg.environmentVariables).not.to.have.property("responseTimeout")
+        expect(cfg.env.baz).to.eq("baz")
+        expect(cfg.env).not.to.have.property("fileServerFolder")
+        expect(cfg.env).not.to.have.property("port")
+        expect(cfg.env).not.to.have.property("BASE_URL")
+        expect(cfg.env).not.to.have.property("watchForFileChanges")
+        expect(cfg.env).not.to.have.property("responseTimeout")
 
         expect(cfg.resolved.fileServerFolder).to.deep.eq({
           value: "foo"
@@ -1112,7 +1147,7 @@ describe "lib/cypress", ->
           value: 5555
           from: "env"
         })
-        expect(cfg.resolved.environmentVariables.baz).to.deep.eq({
+        expect(cfg.resolved.env.baz).to.deep.eq({
           value: "baz"
           from: "cli"
         })
diff --git a/packages/server/test/support/fixtures/projects/plugin-config/cypress.json b/packages/server/test/support/fixtures/projects/plugin-config/cypress.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/packages/server/test/support/fixtures/projects/plugin-config/cypress.json
@@ -0,0 +1 @@
+{}
diff --git a/packages/server/test/support/fixtures/projects/plugin-config/cypress/integration/app_spec.coffee b/packages/server/test/support/fixtures/projects/plugin-config/cypress/integration/app_spec.coffee
new file mode 100644
index 0000000000..8b956d3b0b
--- /dev/null
+++ b/packages/server/test/support/fixtures/projects/plugin-config/cypress/integration/app_spec.coffee
@@ -0,0 +1,14 @@
+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)
+
+it "overrides env", ->
+  ## overrides come from plugins
+  expect(Cypress.env("foo")).to.eq("bar")
+
+  ## overrides come from CLI
+  expect(Cypress.env("bar")).to.eq("bar")
diff --git a/packages/server/test/support/fixtures/projects/plugin-config/cypress/plugins/index.js b/packages/server/test/support/fixtures/projects/plugin-config/cypress/plugins/index.js
new file mode 100644
index 0000000000..95461bde6d
--- /dev/null
+++ b/packages/server/test/support/fixtures/projects/plugin-config/cypress/plugins/index.js
@@ -0,0 +1,12 @@
+module.exports = (on, config) => {
+  return new Promise((resolve) => {
+    setTimeout(resolve, 100)
+  })
+  .then(() => {
+    config.defaultCommandTimeout = 500
+    config.videoCompression = 20
+    config.env.foo = 'bar'
+
+    return config
+  })
+}
diff --git a/packages/server/test/support/helpers/e2e.coffee b/packages/server/test/support/helpers/e2e.coffee
index bf3c4ac194..b9ee52bcf1 100644
--- a/packages/server/test/support/helpers/e2e.coffee
+++ b/packages/server/test/support/helpers/e2e.coffee
@@ -150,7 +150,7 @@ module.exports = {
       args.push("--hosts=#{options.hosts}")
 
     if options.debug
-      args.push("--show-headless-gui")
+      args.push("--headed")
 
     if options.reporter
       args.push("--reporter=#{options.reporter}")
@@ -161,6 +161,12 @@ module.exports = {
     if browser = (env.BROWSER or options.browser)
       args.push("--browser=#{browser}")
 
+    if options.config
+      args.push("--config", options.config)
+
+    if options.env
+      args.push("--env", options.env)
+
     return args
 
   start: (ctx, options = {}) ->
diff --git a/packages/server/test/unit/args_spec.coffee b/packages/server/test/unit/args_spec.coffee
index 5cede440c3..fa5e1b1d75 100644
--- a/packages/server/test/unit/args_spec.coffee
+++ b/packages/server/test/unit/args_spec.coffee
@@ -43,7 +43,7 @@ describe "lib/util/args", ->
   context "--env", ->
     it "converts to object literal", ->
       options = @setup("--env", "foo=bar,version=0.12.1,host=localhost:8888,bar=qux=")
-      expect(options.environmentVariables).to.deep.eq({
+      expect(options.env).to.deep.eq({
         foo: "bar"
         version: "0.12.1"
         host: "localhost:8888"
@@ -86,16 +86,15 @@ describe "lib/util/args", ->
       expect(@setup("--no-record").record).to.be.false
       expect(@setup("--record=false").record).to.be.false
 
-    it "backs up hosts + environmentVariables", ->
+    it "backs up hosts + env", ->
       expect(@obj).to.deep.eq({
         _: []
-        env: process.env.NODE_ENV
         "get-key": true
         getKey: true
         _hosts: "*.foobar.com=127.0.0.1"
         hosts: {"*.foobar.com": "127.0.0.1"}
-        _environmentVariables: "foo=bar,baz=quux,bar=foo=quz"
-        environmentVariables: {
+        _env: "foo=bar,baz=quux,bar=foo=quz"
+        env: {
           foo: "bar"
           baz: "quux"
           bar: "foo=quz"
@@ -114,7 +113,7 @@ describe "lib/util/args", ->
         "--getKey=true"
         "--config=requestTimeout=1234,responseTimeout=9876"
         "--hosts=*.foobar.com=127.0.0.1"
-        "--environmentVariables=foo=bar,baz=quux,bar=foo=quz"
+        "--env=foo=bar,baz=quux,bar=foo=quz"
         "--requestTimeout=1234"
         "--responseTimeout=9876"
       ])
@@ -137,7 +136,6 @@ describe "lib/util/args", ->
           "/Applications/Cypress.app"
           "/Applications/Cypress.app"
         ]
-        env: process.env.NODE_ENV
         appPath: "/Applications/Cypress.app"
         execPath: "/Applications/Cypress.app"
         updating: true
@@ -159,7 +157,6 @@ describe "lib/util/args", ->
           "/Applications/Cypress.app1"
           "/Applications/Cypress.app2"
         ]
-        env: process.env.NODE_ENV
         appPath: "a"
         execPath: "e"
         "app-path": "a"
diff --git a/packages/server/test/unit/config_spec.coffee b/packages/server/test/unit/config_spec.coffee
index 24ba2adcef..d62fa18115 100644
--- a/packages/server/test/unit/config_spec.coffee
+++ b/packages/server/test/unit/config_spec.coffee
@@ -29,7 +29,7 @@ describe "lib/config", ->
       config.get(@projectPath)
       .then (obj) =>
         expect(obj.projectRoot).to.eq(@projectPath)
-        expect(obj.environmentVariables).to.deep.eq({foo: "bar"})
+        expect(obj.env).to.deep.eq({foo: "bar"})
 
     it "sets projectName", ->
       @setup({}, {foo: "bar"})
@@ -481,9 +481,6 @@ describe "lib/config", ->
     it "baseUrl=null", ->
       @defaults "baseUrl", null
 
-    it "env=CYPRESS_ENV", ->
-      @defaults "env", process.env["CYPRESS_ENV"]
-
     it "defaultCommandTimeout=4000", ->
       @defaults "defaultCommandTimeout", 4000
 
@@ -581,12 +578,12 @@ describe "lib/config", ->
 
       config.mergeDefaults(obj)
       .then (cfg) ->
-        expect(cfg.environmentVariables).to.deep.eq({
+        expect(cfg.env).to.deep.eq({
           foo: "bar"
           bar: "baz"
           version: "1.0.1"
         })
-        expect(cfg.env).to.eq(process.env["CYPRESS_ENV"])
+        expect(cfg.cypressEnv).to.eq(process.env["CYPRESS_ENV"])
         expect(cfg).not.to.have.property("envFile")
 
     it "merges env into @config.env", ->
@@ -600,7 +597,7 @@ describe "lib/config", ->
       }
 
       options = {
-        environmentVariables: {
+        env: {
           version: "0.13.1"
           foo: "bar"
         }
@@ -608,7 +605,7 @@ describe "lib/config", ->
 
       config.mergeDefaults(obj, options)
       .then (cfg) ->
-        expect(cfg.environmentVariables).to.deep.eq({
+        expect(cfg.env).to.deep.eq({
           host: "localhost"
           user: "brian"
           version: "0.13.1"
@@ -629,6 +626,7 @@ describe "lib/config", ->
         config.mergeDefaults(obj, options)
         .then (cfg) ->
           expect(cfg.resolved).to.deep.eq({
+            env:                        { }
             port:                       { value: 1234, from: "cli" },
             hosts:                      { value: null, from: "default" }
             reporter:                   { value: "json", from: "cli" },
@@ -651,14 +649,13 @@ describe "lib/config", ->
             fileServerFolder:           { value: "", from: "default" },
             videoRecording:             { value: true, from: "default" }
             videoCompression:           { value: 32, from: "default" }
-            videoUploadOnPasses:       { value: true, from: "default" }
+            videoUploadOnPasses:        { value: true, from: "default" }
             videosFolder:               { value: "cypress/videos", from: "default" },
             supportFile:                { value: "cypress/support", from: "default" },
             pluginsFile:                { value: "cypress/plugins", from: "default" },
             fixturesFolder:             { value: "cypress/fixtures", from: "default" },
             integrationFolder:          { value: "cypress/integration", from: "default" },
             screenshotsFolder:          { value: "cypress/screenshots", from: "default" },
-            environmentVariables:       { }
             testFiles:                  { value: "**/*.*", from: "default" }
           })
 
@@ -675,13 +672,14 @@ describe "lib/config", ->
           envFile: {
             bar: "bar"
           }
-          environmentVariables: {
+        }
+
+        options = {
+          env: {
             baz: "baz"
           }
         }
 
-        options = {}
-
         config.mergeDefaults(obj, options)
         .then (cfg) ->
           expect(cfg.resolved).to.deep.eq({
@@ -715,7 +713,7 @@ describe "lib/config", ->
             integrationFolder:          { value: "cypress/integration", from: "default" },
             screenshotsFolder:          { value: "cypress/screenshots", from: "default" },
             testFiles:                  { value: "**/*.*", from: "default" }
-            environmentVariables:       {
+            env: {
               foo: {
                 value: "foo"
                 from: "config"
@@ -735,6 +733,61 @@ describe "lib/config", ->
             }
           })
 
+  context ".updateWithPluginValues", ->
+    it "is noop when no overrides", ->
+      expect(config.updateWithPluginValues({foo: 'bar'}, null)).to.deep.eq({
+        foo: 'bar'
+      })
+
+    it "updates resolved config values and returns config with overrides", ->
+      cfg = {
+        foo: "bar"
+        baz: "quux"
+        lol: 1234
+        env: {
+          a: "a"
+          b: "b"
+        }
+        resolved: {
+          foo: { value: "bar", from: "default" }
+          baz: { value: "quux", from: "cli" }
+          lol: { value: 1234,  from: "env" }
+          env: {
+            a: { value: "a", from: "config" }
+            b: { value: "b", from: "config" }
+          }
+        }
+      }
+
+      overrides = {
+        baz: "baz"
+        env: {
+          b: "bb"
+          c: "c"
+        }
+      }
+
+      expect(config.updateWithPluginValues(cfg, overrides)).to.deep.eq({
+        foo: "bar"
+        baz: "baz"
+        lol: 1234
+        env: {
+          a: "a"
+          b: "bb"
+          c: "c"
+        }
+        resolved: {
+          foo: { value: "bar", from: "default" }
+          baz: { value: "baz", from: "plugin" }
+          lol: { value: 1234,  from: "env" }
+          env: {
+            a: { value: "a", from: "config" }
+            b: { value: "bb", from: "plugin" }
+            c: { value: "c", from: "plugin" }
+          }
+        }
+      })
+
   context ".parseEnv", ->
     it "merges together env from config, env from file, env from process, and env from CLI", ->
       @sandbox.stub(config, "getProcessEnvVars").returns({version: "0.12.1", user: "bob"})
@@ -752,14 +805,14 @@ describe "lib/config", ->
           user: "brian"
           foo: "bar"
         }
-
-        environmentVariables: {
-          version: "0.14.0"
-          project: "pie"
-        }
       }
 
-      expect(config.parseEnv(obj)).to.deep.eq({
+      envCLI = {
+        version: "0.14.0"
+        project: "pie"
+      }
+
+      expect(config.parseEnv(obj, envCLI)).to.deep.eq({
         version: "0.14.0"
         project: "pie"
         host: "http://localhost:8888"
diff --git a/packages/server/test/unit/modes/headed_spec.coffee b/packages/server/test/unit/modes/headed_spec.coffee
index 74b7b86de6..ca16acdfc1 100644
--- a/packages/server/test/unit/modes/headed_spec.coffee
+++ b/packages/server/test/unit/modes/headed_spec.coffee
@@ -99,17 +99,13 @@ describe "gui/headed", ->
       @sandbox.stub(state, "get").resolves(@state)
 
     it "calls Events.start with options, adding env, onFocusTests, and os", ->
-      env = process.env["CYPRESS_ENV"]
-      process.env["CYPRESS_ENV"] = "development"
       @sandbox.stub(os, "platform").returns("someOs")
       opts = {}
 
       headed.ready(opts).then ->
         expect(Events.start).to.be.called
-        expect(Events.start.lastCall.args[0].env).to.equal("development")
         expect(Events.start.lastCall.args[0].onFocusTests).to.be.a("function")
         expect(Events.start.lastCall.args[0].os).to.equal("someOs")
-        process.env["CYPRESS_ENV"] = env
 
     it "calls menu.set", ->
       headed.ready({}).then ->
diff --git a/packages/server/test/unit/modes/headless_spec.coffee b/packages/server/test/unit/modes/headless_spec.coffee
index 48abff1dcd..344bf537e8 100644
--- a/packages/server/test/unit/modes/headless_spec.coffee
+++ b/packages/server/test/unit/modes/headless_spec.coffee
@@ -33,7 +33,7 @@ describe "lib/modes/headless", ->
 
       options = {
         port: 8080
-        environmentVariables: {foo: "bar"}
+        env: {foo: "bar"}
         projectPath: "/_test-output/path/to/project/foo"
       }
 
@@ -43,7 +43,7 @@ describe "lib/modes/headless", ->
       expect(openProject.create).to.be.calledWithMatch("/_test-output/path/to/project/foo", {
         port: 8080
         projectPath: "/_test-output/path/to/project/foo"
-        environmentVariables: {foo: "bar"}
+        env: {foo: "bar"}
       }, {
         morgan: false
         socketId: 1234
diff --git a/packages/server/test/unit/plugins/child/run_plugins_spec.coffee b/packages/server/test/unit/plugins/child/run_plugins_spec.coffee
index f3ac401465..9fcc388037 100644
--- a/packages/server/test/unit/plugins/child/run_plugins_spec.coffee
+++ b/packages/server/test/unit/plugins/child/run_plugins_spec.coffee
@@ -55,6 +55,21 @@ describe "lib/plugins/child/run_plugins", ->
     snapshot(JSON.stringify(@ipc.send.lastCall.args[3]))
 
   describe "on 'load' message", ->
+    it "sends error if pluginsFile function rejects the promise", (done) ->
+      err = new Error('foo')
+      pluginsFn = @sandbox.stub().rejects(err)
+
+      mockery.registerMock("plugins-file", pluginsFn)
+      @ipc.on.withArgs("load").yields({})
+      runPlugins(@ipc, "plugins-file")
+
+      @ipc.send = (event, errorType, pluginsFile, stack) ->
+        expect(event).to.eq("load:error")
+        expect(errorType).to.eq("PLUGINS_FUNCTION_ERROR")
+        expect(pluginsFile).to.eq("plugins-file")
+        expect(stack).to.eq(err.stack)
+        done()
+
     it "calls function exported by pluginsFile with register function and config", ->
       pluginsFn = @sandbox.spy()
       mockery.registerMock("plugins-file", pluginsFn)
@@ -65,12 +80,19 @@ describe "lib/plugins/child/run_plugins", ->
       expect(pluginsFn.lastCall.args[0]).to.be.a("function")
       expect(pluginsFn.lastCall.args[1]).to.equal(config)
 
-    it "sends error if pluginsFile function throws an error", ->
-      mockery.registerMock("plugins-file", -> foo.bar())
+    it "sends error if pluginsFile function throws an error", (done) ->
+      err = new Error('foo')
+
+      mockery.registerMock "plugins-file", -> throw err
       runPlugins(@ipc, "plugins-file")
       @ipc.on.withArgs("load").yield()
-      expect(@ipc.send).to.be.called
-      snapshot(withoutStack(@ipc.send.lastCall.args[2]))
+
+      @ipc.send = (event, errorType, pluginsFile, stack) ->
+        expect(event).to.eq("load:error")
+        expect(errorType).to.eq("PLUGINS_FUNCTION_ERROR")
+        expect(pluginsFile).to.eq("plugins-file")
+        expect(stack).to.eq(err.stack)
+        done()
 
   describe "on 'execute' message", ->
     beforeEach ->
diff --git a/packages/server/test/unit/plugins/index_spec.coffee b/packages/server/test/unit/plugins/index_spec.coffee
index 6d88ac2b83..d7d50b5c9b 100644
--- a/packages/server/test/unit/plugins/index_spec.coffee
+++ b/packages/server/test/unit/plugins/index_spec.coffee
@@ -61,7 +61,9 @@ describe "lib/plugins/index", ->
 
     describe "loaded message", ->
       beforeEach ->
-        @ipc.on.withArgs("loaded").yields([{
+        @config = {}
+
+        @ipc.on.withArgs("loaded").yields(@config, [{
           event: "some:event"
           callbackId: 0
         }])
@@ -81,16 +83,29 @@ describe "lib/plugins/index", ->
           expect(value).to.equal("value")
 
     describe "load:error message", ->
-      beforeEach ->
-        @ipc.on.withArgs("load:error").yields("PLUGINS_FILE_ERROR", "path/to/pluginsFile.js", "error message")
+      context "PLUGINS_FILE_ERROR", ->
+        beforeEach ->
+          @ipc.on.withArgs("load:error").yields("PLUGINS_FILE_ERROR", "path/to/pluginsFile.js", "error message stack")
 
-      it "rejects plugins.init", ->
-        plugins.init({ pluginsFile: "cypress-plugin" })
-        .catch (err) =>
-          expect(err.message).to.contain("The plugins file is missing or invalid")
-          expect(err.message).to.contain("path/to/pluginsFile.js")
-          expect(err.message).to.contain("The following error was thrown")
-          expect(err.message).to.contain("error message")
+        it "rejects plugins.init", ->
+          plugins.init({ pluginsFile: "cypress-plugin" })
+          .catch (err) =>
+            expect(err.message).to.contain("The plugins file is missing or invalid")
+            expect(err.message).to.contain("path/to/pluginsFile.js")
+            expect(err.message).to.contain("The following error was thrown")
+            expect(err.message).to.contain("error message stack")
+
+      context "PLUGINS_FUNCTION_ERROR", ->
+        beforeEach ->
+          @ipc.on.withArgs("load:error").yields("PLUGINS_FUNCTION_ERROR", "path/to/pluginsFile.js", "error message stack")
+
+        it "rejects plugins.init", ->
+          plugins.init({ pluginsFile: "cypress-plugin" })
+          .catch (err) =>
+            expect(err.message).to.contain("The function exported by the plugins file threw an error.")
+            expect(err.message).to.contain("path/to/pluginsFile.js")
+            expect(err.message).to.contain("The following error was thrown:")
+            expect(err.message).to.contain("error message stack")
 
     describe "error message", ->
       beforeEach ->
diff --git a/packages/server/test/unit/project_spec.coffee b/packages/server/test/unit/project_spec.coffee
index 905ce658fb..e8d067d4af 100644
--- a/packages/server/test/unit/project_spec.coffee
+++ b/packages/server/test/unit/project_spec.coffee
@@ -139,11 +139,13 @@ describe "lib/project", ->
   context "#open", ->
     beforeEach ->
       @sandbox.stub(@project, "watchSettingsAndStartWebsockets").resolves()
-      @sandbox.stub(@project, "watchSupportFile").resolves()
+      @sandbox.stub(@project, "checkSupportFile").resolves()
       @sandbox.stub(@project, "scaffold").resolves()
       @sandbox.stub(@project, "getConfig").resolves(@config)
       @sandbox.stub(Server.prototype, "open").resolves([])
       @sandbox.stub(Server.prototype, "reset")
+      @sandbox.stub(config, "updateWithPluginValues").returns(@config)
+      @sandbox.stub(scaffold, "plugins").resolves()
       @sandbox.stub(plugins, "init").resolves()
 
     it "calls #watchSettingsAndStartWebsockets with options + config", ->
@@ -156,9 +158,9 @@ describe "lib/project", ->
       @project.open().then =>
         expect(@project.scaffold).to.be.calledWith(@config)
 
-    it "calls #watchSupportFile with server config when scaffolding is finished", ->
+    it "calls #checkSupportFile with server config when scaffolding is finished", ->
       @project.open().then =>
-        expect(@project.watchSupportFile).to.be.calledWith(@config)
+        expect(@project.checkSupportFile).to.be.calledWith(@config)
 
     it "calls #getConfig options", ->
       opts = {}
@@ -167,7 +169,11 @@ describe "lib/project", ->
 
     it "initializes the plugins", ->
       @project.open({}).then =>
-        expect(plugins.init).to.be.calledWith(@config)
+        expect(plugins.init).to.be.called
+
+    it "calls support.plugins with pluginsFile directory", ->
+      @project.open({}).then =>
+        expect(scaffold.plugins).to.be.calledWith(path.dirname(@config.pluginsFile))
 
     it "calls options.onError with plugins error when there is a plugins error", ->
       onError = @sandbox.spy()
@@ -263,10 +269,6 @@ describe "lib/project", ->
       @project.scaffold(@obj).then =>
         expect(scaffold.support).to.be.calledWith(@obj.supportFolder)
 
-    it "calls support.plugins with pluginsFile directory", ->
-      @project.scaffold(@obj).then =>
-        expect(scaffold.plugins).to.be.calledWith("pf")
-
     it "does not call support.plugins if config.pluginsFile is falsey", ->
       @obj.pluginsFile = false
       @project.scaffold(@obj).then =>
@@ -314,7 +316,7 @@ describe "lib/project", ->
 
       expect(stub).to.be.calledOnce
 
-  context "#watchSupportFile", ->
+  context "#checkSupportFile", ->
     beforeEach ->
       @sandbox.stub(fs, "pathExists").resolves(true)
       @project = Project("/_test-output/path/to/project")
@@ -326,30 +328,13 @@ describe "lib/project", ->
       }
 
     it "does nothing when {supportFile: false}", ->
-      @project.watchSupportFile({supportFile: false})
+      ret = @project.checkSupportFile({supportFile: false})
 
-      expect(preprocessor.getFile).not.to.be.called
-
-    it "calls preprocessor.getFile with relative path to file", ->
-      @project.watchSupportFile(@config)
-      .then () =>
-        expect(preprocessor.getFile).to.be.calledWith("foo/bar.js", @config)
-
-    it "calls server.onTestFileChange when file changes", ->
-      @project.watchSupportFile(@config)
-      .then () =>
-        preprocessor.getFile.firstCall.args[2].onChange()
-        expect(@project.server.onTestFileChange).to.be.calledWith("foo/bar.js")
-
-    it "does not add change listener when {watchForFileChanges: false}", ->
-      @config.watchForFileChanges = false
-      @project.watchSupportFile(@config)
-      .then () =>
-        expect(preprocessor.getFile.firstCall.args[2]).to.be.undefined
+      expect(ret).to.be.undefined
 
     it "throws when support file does not exist", ->
       fs.pathExists.resolves(false)
-      @project.watchSupportFile(@config)
+      @project.checkSupportFile(@config)
       .catch (e) ->
         expect(e.message).to.include("The support file is missing or invalid.")