mirror of
https://github.com/vuejs/vue-cli.git
synced 2026-01-16 04:10:20 -06:00
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:
@@ -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()
|
||||
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user