From d595adacf454cfcf9250eac11993bcac0331c52e Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 1 May 2018 17:14:33 -0400 Subject: [PATCH] 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 --- docs/plugin-dev.md | 48 +++++----- packages/@vue/cli-plugin-e2e-cypress/index.js | 7 +- .../@vue/cli-plugin-e2e-nightwatch/index.js | 6 +- .../__tests__/tsPluginBabel.spec.js | 1 + packages/@vue/cli-plugin-unit-jest/index.js | 5 +- packages/@vue/cli-plugin-unit-mocha/index.js | 5 +- .../cli-service/__tests__/Service.spec.js | 82 +++++++++-------- .../@vue/cli-service/__tests__/css.spec.js | 4 +- packages/@vue/cli-service/lib/PluginAPI.js | 27 ++---- packages/@vue/cli-service/lib/Service.js | 87 ++++++++++++++----- .../cli-service/lib/commands/build/index.js | 9 +- .../@vue/cli-service/lib/commands/inspect.js | 6 +- .../@vue/cli-service/lib/commands/serve.js | 9 +- packages/@vue/cli-service/webpack.config.js | 1 + 14 files changed, 179 insertions(+), 118 deletions(-) diff --git a/docs/plugin-dev.md b/docs/plugin-dev.md index 8c673a005..ad00933bc 100644 --- a/docs/plugin-dev.md +++ b/docs/plugin-dev.md @@ -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() diff --git a/packages/@vue/cli-plugin-e2e-cypress/index.js b/packages/@vue/cli-plugin-e2e-cypress/index.js index cd1104288..d0055857f 100644 --- a/packages/@vue/cli-plugin-e2e-cypress/index.js +++ b/packages/@vue/cli-plugin-e2e-cypress/index.js @@ -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' +} diff --git a/packages/@vue/cli-plugin-e2e-nightwatch/index.js b/packages/@vue/cli-plugin-e2e-nightwatch/index.js index e2a426609..bb8668508 100644 --- a/packages/@vue/cli-plugin-e2e-nightwatch/index.js +++ b/packages/@vue/cli-plugin-e2e-nightwatch/index.js @@ -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' +} diff --git a/packages/@vue/cli-plugin-typescript/__tests__/tsPluginBabel.spec.js b/packages/@vue/cli-plugin-typescript/__tests__/tsPluginBabel.spec.js index ff0ab4c2f..d590f839f 100644 --- a/packages/@vue/cli-plugin-typescript/__tests__/tsPluginBabel.spec.js +++ b/packages/@vue/cli-plugin-typescript/__tests__/tsPluginBabel.spec.js @@ -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') diff --git a/packages/@vue/cli-plugin-unit-jest/index.js b/packages/@vue/cli-plugin-unit-jest/index.js index d46b7d103..01cf34da0 100644 --- a/packages/@vue/cli-plugin-unit-jest/index.js +++ b/packages/@vue/cli-plugin-unit-jest/index.js @@ -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' +} diff --git a/packages/@vue/cli-plugin-unit-mocha/index.js b/packages/@vue/cli-plugin-unit-mocha/index.js index 59a0c6617..29277ff93 100644 --- a/packages/@vue/cli-plugin-unit-mocha/index.js +++ b/packages/@vue/cli-plugin-unit-mocha/index.js @@ -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' +} diff --git a/packages/@vue/cli-service/__tests__/Service.spec.js b/packages/@vue/cli-service/__tests__/Service.spec.js index 25941b694..a5dc91f48 100644 --- a/packages/@vue/cli-service/__tests__/Service.spec.js +++ b/packages/@vue/cli-service/__tests__/Service.spec.js @@ -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', diff --git a/packages/@vue/cli-service/__tests__/css.spec.js b/packages/@vue/cli-service/__tests__/css.spec.js index 2aeb891d3..8e6e08857 100644 --- a/packages/@vue/cli-service/__tests__/css.spec.js +++ b/packages/@vue/cli-service/__tests__/css.spec.js @@ -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 } diff --git a/packages/@vue/cli-service/lib/PluginAPI.js b/packages/@vue/cli-service/lib/PluginAPI.js index 8926fe8c3..66c8b7139 100644 --- a/packages/@vue/cli-service/lib/PluginAPI.js +++ b/packages/@vue/cli-service/lib/PluginAPI.js @@ -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. diff --git a/packages/@vue/cli-service/lib/Service.js b/packages/@vue/cli-service/lib/Service.js index 7964e7f30..88514658c 100644 --- a/packages/@vue/cli-service/lib/Service.js +++ b/packages/@vue/cli-service/lib/Service.js @@ -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' } diff --git a/packages/@vue/cli-service/lib/commands/build/index.js b/packages/@vue/cli-service/lib/commands/build/index.js index 0c55d77cf..a2cfc274b 100644 --- a/packages/@vue/cli-service/lib/commands/build/index.js +++ b/packages/@vue/cli-service/lib/commands/build/index.js @@ -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' +} diff --git a/packages/@vue/cli-service/lib/commands/inspect.js b/packages/@vue/cli-service/lib/commands/inspect.js index 8d6e8d1dd..7ec8d987e 100644 --- a/packages/@vue/cli-service/lib/commands/inspect.js +++ b/packages/@vue/cli-service/lib/commands/inspect.js @@ -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' +} diff --git a/packages/@vue/cli-service/lib/commands/serve.js b/packages/@vue/cli-service/lib/commands/serve.js index d1ce8ce92..c8d3a9fad 100644 --- a/packages/@vue/cli-service/lib/commands/serve.js +++ b/packages/@vue/cli-service/lib/commands/serve.js @@ -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' +} diff --git a/packages/@vue/cli-service/webpack.config.js b/packages/@vue/cli-service/webpack.config.js index c1e3bd6be..93dd6f3ff 100644 --- a/packages/@vue/cli-service/webpack.config.js +++ b/packages/@vue/cli-service/webpack.config.js @@ -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()