refactor: adjust mode loading order

BREAKING CHANGE: PluginAPI.setMode() has been removed. Instead, for a plugin to
sepcify the default mode for a registered command, the plugins should expose
`module.exports.defaultModes` in the form of `{ [commandName]: mode }`.

close #959
This commit is contained in:
Evan You
2018-05-01 17:14:33 -04:00
parent 51c8090166
commit d595adacf4
14 changed files with 179 additions and 118 deletions

View File

@@ -72,54 +72,52 @@ module.exports = (api, projectOptions) => {
}
```
#### Environment Variables in Service Plugins
#### Specifying Mode for Commands
An important thing to note about env variables is knowing when they are resolved. Typically, a command like `vue-cli-service serve` or `vue-cli-service build` will always call `api.setMode()` as the first thing it does. However, this also means those env variables may not yet be available when a service plugin is invoked:
> Note: the way plugins set modes has been changed in beta.10.
If a plugin-registered command needs to run in a specific default mode,
the plugin needs to expose it via `module.exports.defaultModes` in the form
of `{ [commandName]: mode }`:
``` js
module.exports = api => {
process.env.NODE_ENV // may not be resolved yet
api.registerCommand('build', () => {
api.setMode('production')
// ...
})
}
module.exports.defaultModes = {
build: 'production'
}
```
Instead, it's safer to rely on env variables in `configureWebpack` or `chainWebpack` functions, which are called lazily only when `api.resolveWebpackConfig()` is finally called:
``` js
module.exports = api => {
api.configureWebpack(config => {
if (process.env.NODE_ENV === 'production') {
// ...
}
})
}
```
This is because the command's expected mode needs to be known before loading environment variables, which in turn needs to happen before loading user options / applying the plugins.
#### Resolving Webpack Config in Plugins
A plugin can retrieve the resolved webpack config by calling `api.resolveWebpackConfig()`. Every call generates a fresh webpack config which can be further mutated as needed:
``` js
api.registerCommand('my-build', args => {
// make sure to set mode and load env variables
api.setMode('production')
module.exports = api => {
api.registerCommand('my-build', args => {
const configA = api.resolveWebpackConfig()
const configB = api.resolveWebpackConfig()
const configA = api.resolveWebpackConfig()
const configB = api.resolveWebpackConfig()
// mutate configA and configB for different purposes...
})
}
// mutate configA and configB for different purposes...
})
// make sure to specify the default mode for correct env variables
module.exports.defaultModes = {
'my-build': 'production'
}
```
Alternatively, a plugin can also obtain a fresh [chainable config](https://github.com/mozilla-neutrino/webpack-chain) by calling `api.resolveChainableWebpackConfig()`:
``` js
api.registerCommand('my-build', args => {
api.setMode('production')
const configA = api.resolveChainableWebpackConfig()
const configB = api.resolveChainableWebpackConfig()

View File

@@ -16,7 +16,7 @@ module.exports = (api, options) => {
const serverPromise = args.url
? Promise.resolve({ url: args.url })
: api.service.run('serve', { mode: args.mode || 'production' })
: api.service.run('serve')
return serverPromise.then(({ url, server }) => {
const { info } = require('@vue/cli-shared-utils')
@@ -71,3 +71,8 @@ module.exports = (api, options) => {
chalk.yellow(`https://docs.cypress.io/guides/guides/command-line.html#cypress-open`)
}, (args, rawArgs) => run('open', args, rawArgs))
}
module.exports.defaultModes = {
e2e: 'production',
'e2e:open': 'production'
}

View File

@@ -27,7 +27,7 @@ module.exports = (api, options) => {
const serverPromise = args.url
? Promise.resolve({ url: args.url })
: api.service.run('serve', { mode: args.mode || 'production' })
: api.service.run('serve')
return serverPromise.then(({ server, url }) => {
// expose dev server url to tests
@@ -68,3 +68,7 @@ module.exports = (api, options) => {
})
})
}
module.exports.defaultModes = {
e2e: 'production'
}

View File

@@ -12,6 +12,7 @@ test('using correct loader', () => {
]
})
service.init()
const config = service.resolveWebpackConfig()
const rule = config.module.rules.find(rule => rule.test.test('foo.ts'))
expect(rule.use[0].loader).toMatch('cache-loader')

View File

@@ -9,7 +9,6 @@ module.exports = api => {
`All jest command line options are supported.\n` +
`See https://facebook.github.io/jest/docs/en/cli.html for more details.`
}, (args, rawArgv) => {
api.setMode('test')
// for @vue/babel-preset-app
process.env.VUE_CLI_BABEL_TARGET_NODE = true
process.env.VUE_CLI_BABEL_TRANSPILE_MODULES = true
@@ -33,3 +32,7 @@ module.exports = api => {
})
})
}
module.exports.defaultModes = {
test: 'test'
}

View File

@@ -40,7 +40,6 @@ module.exports = api => {
`http://zinserjan.github.io/mocha-webpack/docs/installation/cli-usage.html`
)
}, (args, rawArgv) => {
api.setMode('test')
// for @vue/babel-preset-app
process.env.VUE_CLI_BABEL_TARGET_NODE = true
// start runner
@@ -74,3 +73,7 @@ module.exports = api => {
})
})
}
module.exports.defaultModes = {
test: 'test'
}

View File

@@ -10,10 +10,16 @@ const mockPkg = json => {
fs.writeFileSync('/package.json', JSON.stringify(json, null, 2))
}
const createMockService = (plugins = []) => new Service('/', {
plugins,
useBuiltIn: false
})
const createMockService = (plugins = [], init = true) => {
const service = new Service('/', {
plugins,
useBuiltIn: false
})
if (init) {
service.init()
}
return service
}
beforeEach(() => {
mockPkg({})
@@ -79,36 +85,6 @@ test('load project options from vue.config.js', () => {
expect(service.projectOptions.lintOnSave).toBe(false)
})
test('api: setMode', () => {
fs.writeFileSync('/.env.foo', `FOO=5\nBAR=6`)
fs.writeFileSync('/.env.foo.local', `FOO=7\nBAZ=8`)
createMockService([{
id: 'test-setMode',
apply: api => {
api.setMode('foo')
}
}])
expect(process.env.FOO).toBe('7')
expect(process.env.BAR).toBe('6')
expect(process.env.BAZ).toBe('8')
expect(process.env.VUE_CLI_MODE).toBe('foo')
// for NODE_ENV & BABEL_ENV
// any mode that is not test or production defaults to development
expect(process.env.NODE_ENV).toBe('development')
expect(process.env.BABEL_ENV).toBe('development')
createMockService([{
id: 'test-setMode',
apply: api => {
api.setMode('test')
}
}])
expect(process.env.VUE_CLI_MODE).toBe('test')
expect(process.env.NODE_ENV).toBe('test')
expect(process.env.BABEL_ENV).toBe('test')
})
test('api: registerCommand', () => {
let args
const service = createMockService([{
@@ -124,6 +100,44 @@ test('api: registerCommand', () => {
expect(args).toEqual({ _: [], n: 1 })
})
test('api: defaultModes', () => {
fs.writeFileSync('/.env.foo', `FOO=5\nBAR=6`)
fs.writeFileSync('/.env.foo.local', `FOO=7\nBAZ=8`)
const plugin1 = {
id: 'test-defaultModes',
apply: api => {
expect(process.env.FOO).toBe('7')
expect(process.env.BAR).toBe('6')
expect(process.env.BAZ).toBe('8')
// for NODE_ENV & BABEL_ENV
// any mode that is not test or production defaults to development
expect(process.env.NODE_ENV).toBe('development')
expect(process.env.BABEL_ENV).toBe('development')
api.registerCommand('foo', () => {})
}
}
plugin1.apply.defaultModes = {
foo: 'foo'
}
createMockService([plugin1], false /* init */).run('foo')
const plugin2 = {
id: 'test-defaultModes',
apply: api => {
expect(process.env.NODE_ENV).toBe('test')
expect(process.env.BABEL_ENV).toBe('test')
api.registerCommand('test', () => {})
}
}
plugin2.apply.defaultModes = {
test: 'test'
}
createMockService([plugin2], false /* init */).run('test')
})
test('api: chainWebpack', () => {
const service = createMockService([{
id: 'test',

View File

@@ -14,7 +14,9 @@ const LOADERS = {
const genConfig = (pkg = {}, env) => {
const prevEnv = process.env.NODE_ENV
if (env) process.env.NODE_ENV = env
const config = new Service('/', { pkg }).resolveWebpackConfig()
const service = new Service('/', { pkg })
service.init()
const config = service.resolveWebpackConfig()
process.env.NODE_ENV = prevEnv
return config
}

View File

@@ -1,6 +1,11 @@
const path = require('path')
const { matchesPluginId } = require('@vue/cli-shared-utils')
// Note: if a plugin-registered command needs to run in a specific default mode,
// the plugin needs to expose it via `module.exports.defaultModes` in the form
// of { [commandName]: mode }. This is because the command mode needs to be
// known and applied before loading user options / applying plugins.
class PluginAPI {
/**
* @param {string} id - Id of the plugin.
@@ -31,25 +36,6 @@ class PluginAPI {
return this.service.plugins.some(p => matchesPluginId(id, p.id))
}
/**
* Set project mode and resolve env variables for that mode.
* this should be called by any registered command as early as possible, and
* should be called only once per command.
*
* @param {string} mode
*/
setMode (mode) {
process.env.VUE_CLI_MODE = mode
// by default, NODE_ENV and BABEL_ENV are set to "development" unless mode
// is production or test. However this can be overwritten in .env files.
process.env.NODE_ENV = process.env.BABEL_ENV =
(mode === 'production' || mode === 'test')
? mode
: 'development'
// load .env files based on mode
this.service.loadEnv(mode)
}
/**
* Register a command that will become available as `vue-cli-service [name]`.
*
@@ -68,7 +54,7 @@ class PluginAPI {
fn = opts
opts = null
}
this.service.commands[name] = { fn, opts }
this.service.commands[name] = { fn, opts: opts || {}}
}
/**
@@ -108,7 +94,6 @@ class PluginAPI {
/**
* Resolve the final raw webpack config, that will be passed to webpack.
* Typically, you should call `setMode` before calling this.
*
* @param {ChainableWebpackConfig} [chainableConfig]
* @return {object} Raw webpack config.

View File

@@ -13,29 +13,60 @@ const { warn, error, isPlugin } = require('@vue/cli-shared-utils')
const { defaults, validate } = require('./options')
module.exports = class Service {
constructor (context, { plugins, pkg, projectOptions, useBuiltIn } = {}) {
constructor (context, { plugins, pkg, inlineOptions, useBuiltIn } = {}) {
process.VUE_CLI_SERVICE = this
this.initialized = false
this.context = context
this.inlineOptions = inlineOptions
this.webpackChainFns = []
this.webpackRawConfigFns = []
this.devServerConfigFns = []
this.commands = {}
this.pkg = this.resolvePkg(pkg)
// load base .env
this.loadEnv()
const userOptions = this.loadUserOptions(projectOptions)
this.projectOptions = defaultsDeep(userOptions, defaults())
debug('vue:project-config')(this.projectOptions)
// install plugins.
// If there are inline plugins, they will be used instead of those
// found in package.json.
// When useBuiltIn === false, built-in plugins are disabled. This is mostly
// for testing.
this.plugins = this.resolvePlugins(plugins, useBuiltIn)
// resolve the default mode to use for each command
// this is provided by plugins as module.exports.defaulModes
// so we can get the information without actually applying the plugin.
this.modes = this.plugins.reduce((modes, { apply: { defaultModes }}) => {
return Object.assign(modes, defaultModes)
}, {})
}
resolvePkg (inlinePkg) {
if (inlinePkg) {
return inlinePkg
} else if (fs.existsSync(path.join(this.context, 'package.json'))) {
return readPkg.sync(this.context)
} else {
return {}
}
}
init (mode = process.env.VUE_CLI_MODE) {
if (this.initialized) {
return
}
this.initialized = true
this.mode = mode
// load base .env
this.loadEnv()
// load mode .env
if (mode) {
this.loadEnv(mode)
}
// load user config
const userOptions = this.loadUserOptions()
this.projectOptions = defaultsDeep(userOptions, defaults())
debug('vue:project-config')(this.projectOptions)
// apply plugins.
this.plugins.forEach(({ id, apply }) => {
apply(new PluginAPI(id, this), this.projectOptions)
})
@@ -49,17 +80,16 @@ module.exports = class Service {
}
}
resolvePkg (inlinePkg) {
if (inlinePkg) {
return inlinePkg
} else if (fs.existsSync(path.join(this.context, 'package.json'))) {
return readPkg.sync(this.context)
} else {
return {}
}
}
loadEnv (mode) {
if (mode) {
// by default, NODE_ENV and BABEL_ENV are set to "development" unless mode
// is production or test. However this can be overwritten in .env files.
process.env.NODE_ENV = process.env.BABEL_ENV =
(mode === 'production' || mode === 'test')
? mode
: 'development'
}
const logger = debug('vue:env')
const basePath = path.resolve(this.context, `.env${mode ? `.${mode}` : ``}`)
const localPath = `${basePath}.local`
@@ -113,6 +143,14 @@ module.exports = class Service {
}
async run (name, args = {}, rawArgv = []) {
// resolve mode
// prioritize inline --mode
// fallback to resolved default modes from plugins
const mode = args.mode || this.modes[name]
// load env variables, load user config, apply plugins
this.init(mode)
args._ = args._ || []
let command = this.commands[name]
if (!command && name) {
@@ -137,6 +175,9 @@ module.exports = class Service {
}
resolveWebpackConfig (chainableConfig = this.resolveChainableWebpackConfig()) {
if (!this.initialized) {
throw new Error('Service must call init() before calling resolveWebpackConfig().')
}
// get raw config
let config = chainableConfig.toConfig()
// apply raw config fns
@@ -153,7 +194,7 @@ module.exports = class Service {
return config
}
loadUserOptions (inlineOptions) {
loadUserOptions () {
// vue.config.js
let fileConfig, pkgConfig, resolved, resovledFrom
const configPath = (
@@ -202,7 +243,7 @@ module.exports = class Service {
resolved = pkgConfig
resovledFrom = '"vue" field in package.json'
} else {
resolved = inlineOptions || {}
resolved = this.inlineOptions || {}
resovledFrom = 'inline options'
}

View File

@@ -1,5 +1,4 @@
const defaults = {
mode: 'production',
target: 'app',
entry: 'src/App.vue'
}
@@ -15,7 +14,7 @@ module.exports = (api, options) => {
description: 'build for production',
usage: 'vue-cli-service build [options] [entry|pattern]',
options: {
'--mode': `specify env mode (default: ${defaults.mode})`,
'--mode': `specify env mode (default: production)`,
'--dest': `specify output directory (default: ${options.outputDir})`,
'--target': `app | lib | wc | wc-async (default: ${defaults.target})`,
'--name': `name for lib or web-component mode (default: "name" in package.json or entry filename)`
@@ -28,8 +27,6 @@ module.exports = (api, options) => {
}
}
api.setMode(args.mode)
const fs = require('fs-extra')
const path = require('path')
const chalk = require('chalk')
@@ -165,3 +162,7 @@ module.exports = (api, options) => {
})
})
}
module.exports.defaultModes = {
build: 'production'
}

View File

@@ -7,8 +7,6 @@ module.exports = (api, options) => {
'--verbose': 'show full function definitions in output'
}
}, args => {
api.setMode(args.mode || 'development')
const get = require('get-value')
const stringify = require('javascript-stringify')
const config = api.resolveWebpackConfig()
@@ -43,3 +41,7 @@ module.exports = (api, options) => {
}, 2))
})
}
module.exports.defaultModes = {
inspect: 'development'
}

View File

@@ -5,7 +5,6 @@ const {
} = require('@vue/cli-shared-utils')
const defaults = {
mode: 'development',
host: '0.0.0.0',
port: 8080,
https: false
@@ -17,7 +16,7 @@ module.exports = (api, options) => {
usage: 'vue-cli-service serve [options]',
options: {
'--open': `open browser on server start`,
'--mode': `specify env mode (default: ${defaults.mode})`,
'--mode': `specify env mode (default: development)`,
'--host': `specify host (default: ${defaults.host})`,
'--port': `specify port (default: ${defaults.port})`,
'--https': `use https (default: ${defaults.https})`
@@ -25,8 +24,6 @@ module.exports = (api, options) => {
}, async function serve (args) {
info('Starting development server...')
api.setMode(args.mode || defaults.mode)
// although this is primarily a dev server, it is possible that we
// are running it in a mode with a production env, e.g. in E2E tests.
const isProduction = process.env.NODE_ENV === 'production'
@@ -207,3 +204,7 @@ function addDevClientToEntry (config, devClient) {
config.entry = devClient.concat(entry)
}
}
module.exports.defaultModes = {
serve: 'development'
}

View File

@@ -6,6 +6,7 @@ let service = process.VUE_CLI_SERVICE
if (!service) {
const Service = require('./lib/Service')
service = new Service(process.env.VUE_CLI_CONTEXT || process.cwd())
service.init()
}
module.exports = service.resolveWebpackConfig()