diff --git a/docs/dev-guide/generator-api.md b/docs/dev-guide/generator-api.md index 20b7b9b4b2..4dfa51cc6b 100644 --- a/docs/dev-guide/generator-api.md +++ b/docs/dev-guide/generator-api.md @@ -54,12 +54,13 @@ Resolve a path for the current project - **Arguments** - `{string} id` - plugin id, can omit the (@vue/|vue-|@scope/vue)-cli-plugin- prefix + - `{string} version` - semver version range, optional - **Returns** - `{boolean}` - **Usage**: -Check if the project has a plugin with given id +Check if the project has a plugin with given id. If version range is given, then the plugin version should satisfy it ## addConfigTransform @@ -177,4 +178,3 @@ Get the entry file taking into account typescript. - **Usage**: Checks if the plugin is being invoked. - diff --git a/docs/dev-guide/plugin-dev.md b/docs/dev-guide/plugin-dev.md index 8dbbda2229..e064bf9d97 100644 --- a/docs/dev-guide/plugin-dev.md +++ b/docs/dev-guide/plugin-dev.md @@ -241,51 +241,57 @@ Let's consider the case where we have created a `router.js` file via [templating api.injectImports(api.entryFile, `import router from './router'`) ``` -Now, when we have a router imported, we can inject this router to the Vue instance in the main file. We will use `onCreateComplete` hook which is to be called when the files have been written to disk. +Now, when we have a router imported, we can inject this router to the Vue instance in the main file. We will use `afterInvoke` hook which is to be called when the files have been written to disk. First, we need to read main file content with Node `fs` module (which provides an API for interacting with the file system) and split this content on lines: ```js // generator/index.js -api.onCreateComplete(() => { - const fs = require('fs') - const contentMain = fs.readFileSync(api.entryFile, { encoding: 'utf-8' }) - const lines = contentMain.split(/\r?\n/g) -}) +module.exports.hooks = (api) => { + api.afterInvoke(() => { + const fs = require('fs') + const contentMain = fs.readFileSync(api.entryFile, { encoding: 'utf-8' }) + const lines = contentMain.split(/\r?\n/g) + }) +} ``` Then we should to find the string containing `render` word (it's usually a part of Vue instance) and add our `router` as a next string: -```js{8-9} +```js{9-10} // generator/index.js -api.onCreateComplete(() => { - const fs = require('fs') - const contentMain = fs.readFileSync(api.entryFile, { encoding: 'utf-8' }) - const lines = contentMain.split(/\r?\n/g) +module.exports.hooks = (api) => { + api.afterInvoke(() => { + const fs = require('fs') + const contentMain = fs.readFileSync(api.entryFile, { encoding: 'utf-8' }) + const lines = contentMain.split(/\r?\n/g) - const renderIndex = lines.findIndex(line => line.match(/render/)) - lines[renderIndex] += `\n router,` -}) + const renderIndex = lines.findIndex(line => line.match(/render/)) + lines[renderIndex] += `\n router,` + }) +} ``` Finally, you need to write the content back to the main file: -```js{2,11} +```js{12-13} // generator/index.js -api.onCreateComplete(() => { - const { EOL } = require('os') - const fs = require('fs') - const contentMain = fs.readFileSync(api.entryFile, { encoding: 'utf-8' }) - const lines = contentMain.split(/\r?\n/g) +module.exports.hooks = (api) => { + api.afterInvoke(() => { + const { EOL } = require('os') + const fs = require('fs') + const contentMain = fs.readFileSync(api.entryFile, { encoding: 'utf-8' }) + const lines = contentMain.split(/\r?\n/g) - const renderIndex = lines.findIndex(line => line.match(/render/)) - lines[renderIndex] += `${EOL} router,` + const renderIndex = lines.findIndex(line => line.match(/render/)) + lines[renderIndex] += `${EOL} router,` - fs.writeFileSync(api.entryFile, lines.join(EOL), { encoding: 'utf-8' }) -}) + fs.writeFileSync(api.entryFile, lines.join(EOL), { encoding: 'utf-8' }) + }) +} ``` ## Service Plugin diff --git a/packages/@vue/cli-plugin-eslint/generator/index.js b/packages/@vue/cli-plugin-eslint/generator/index.js index 8a4b10e23d..b34d49ab7e 100644 --- a/packages/@vue/cli-plugin-eslint/generator/index.js +++ b/packages/@vue/cli-plugin-eslint/generator/index.js @@ -2,6 +2,9 @@ const fs = require('fs') const path = require('path') module.exports = (api, { config, lintOn = [] }, _, invoking) => { + api.assertCliVersion('^4.0.0-alpha.4') + api.assertCliServiceVersion('^4.0.0-alpha.4') + if (typeof lintOn === 'string') { lintOn = lintOn.split(',') } @@ -97,13 +100,13 @@ module.exports = (api, { config, lintOn = [] }, _, invoking) => { require('@vue/cli-plugin-unit-jest/generator').applyESLint(api) } } +} +module.exports.hooks = (api) => { // lint & fix after create to ensure files adhere to chosen config - if (config && config !== 'base') { - api.onCreateComplete(() => { - require('../lint')({ silent: true }, api) - }) - } + api.afterAnyInvoke(() => { + require('../lint')({ silent: true }, api) + }) } const applyTS = module.exports.applyTS = api => { diff --git a/packages/@vue/cli/__tests__/Generator.spec.js b/packages/@vue/cli/__tests__/Generator.spec.js index 02119f0682..263aa4a792 100644 --- a/packages/@vue/cli/__tests__/Generator.spec.js +++ b/packages/@vue/cli/__tests__/Generator.spec.js @@ -448,7 +448,24 @@ test('api: onCreateComplete', () => { } } ], - completeCbs: cbs + afterInvokeCbs: cbs + }) + expect(cbs).toContain(fn) +}) + +test('api: afterInvoke', () => { + const fn = () => {} + const cbs = [] + new Generator('/', { + plugins: [ + { + id: 'test', + apply: api => { + api.afterInvoke(fn) + } + } + ], + afterInvokeCbs: cbs }) expect(cbs).toContain(fn) }) diff --git a/packages/@vue/cli/lib/Creator.js b/packages/@vue/cli/lib/Creator.js index 9a0cd6d906..a2897f27ca 100644 --- a/packages/@vue/cli/lib/Creator.js +++ b/packages/@vue/cli/lib/Creator.js @@ -54,7 +54,8 @@ module.exports = class Creator extends EventEmitter { this.outroPrompts = this.resolveOutroPrompts() this.injectedPrompts = [] this.promptCompleteCbs = [] - this.createCompleteCbs = [] + this.afterInvokeCbs = [] + this.afterAnyInvokeCbs = [] this.run = this.run.bind(this) @@ -64,7 +65,7 @@ module.exports = class Creator extends EventEmitter { async create (cliOptions = {}, preset = null) { const isTestOrDebug = process.env.VUE_CLI_TEST || process.env.VUE_CLI_DEBUG - const { run, name, context, createCompleteCbs } = this + const { run, name, context, afterInvokeCbs, afterAnyInvokeCbs } = this if (!preset) { if (cliOptions.preset) { @@ -187,7 +188,8 @@ module.exports = class Creator extends EventEmitter { const generator = new Generator(context, { pkg, plugins, - completeCbs: createCompleteCbs + afterInvokeCbs, + afterAnyInvokeCbs }) await generator.generate({ extractConfigFiles: preset.useConfigFiles @@ -204,7 +206,10 @@ module.exports = class Creator extends EventEmitter { // run complete cbs if any (injected by generators) logWithSpinner('⚓', `Running completion hooks...`) this.emit('creation', { event: 'completion-hooks' }) - for (const cb of createCompleteCbs) { + for (const cb of afterInvokeCbs) { + await cb() + } + for (const cb of afterAnyInvokeCbs) { await cb() } diff --git a/packages/@vue/cli/lib/Generator.js b/packages/@vue/cli/lib/Generator.js index 0955b212de..b99dcebd28 100644 --- a/packages/@vue/cli/lib/Generator.js +++ b/packages/@vue/cli/lib/Generator.js @@ -1,12 +1,14 @@ const ejs = require('ejs') const debug = require('debug') +const semver = require('semver') const GeneratorAPI = require('./GeneratorAPI') +const PackageManager = require('./util/ProjectPackageManager') const sortObject = require('./util/sortObject') const writeFileTree = require('./util/writeFileTree') const inferRootOptions = require('./util/inferRootOptions') const normalizeFilePaths = require('./util/normalizeFilePaths') const runCodemod = require('./util/runCodemod') -const { toShortPluginId, matchesPluginId } = require('@vue/cli-shared-utils') +const { toShortPluginId, matchesPluginId, loadModule, isPlugin } = require('@vue/cli-shared-utils') const ConfigTransform = require('./ConfigTransform') const logger = require('@vue/cli-shared-utils/lib/logger') @@ -69,7 +71,8 @@ module.exports = class Generator { constructor (context, { pkg = {}, plugins = [], - completeCbs = [], + afterInvokeCbs = [], + afterAnyInvokeCbs = [], files = {}, invoking = false } = {}) { @@ -77,9 +80,11 @@ module.exports = class Generator { this.plugins = plugins this.originalPkg = pkg this.pkg = Object.assign({}, pkg) + this.pm = new PackageManager({ context }) this.imports = {} this.rootOptions = {} - this.completeCbs = completeCbs + this.afterInvokeCbs = [] + this.afterAnyInvokeCbs = afterAnyInvokeCbs this.configTransforms = {} this.defaultConfigTransforms = defaultConfigTransforms this.reservedConfigTransforms = reservedConfigTransforms @@ -93,15 +98,49 @@ module.exports = class Generator { // exit messages this.exitLogs = [] + const pluginIds = plugins.map(p => p.id) + + // load all the other plugins + this.allPlugins = Object.keys(this.pkg.dependencies || {}) + .concat(Object.keys(this.pkg.devDependencies || {})) + .filter(isPlugin) + const cliService = plugins.find(p => p.id === '@vue/cli-service') const rootOptions = cliService ? cliService.options : inferRootOptions(pkg) + + // apply hooks from all plugins + this.allPlugins.forEach(id => { + const api = new GeneratorAPI(id, this, {}, rootOptions) + const pluginGenerator = loadModule(`${id}/generator`, context) + + if (pluginGenerator && pluginGenerator.hooks) { + pluginGenerator.hooks(api, {}, rootOptions, pluginIds) + } + }) + + // We are doing save/load to make the hook order deterministic + // save "any" hooks + const afterAnyInvokeCbsFromPlugins = this.afterAnyInvokeCbs + + // reset hooks + this.afterInvokeCbs = afterInvokeCbs + this.afterAnyInvokeCbs = [] + this.postProcessFilesCbs = [] + // apply generators from plugins plugins.forEach(({ id, apply, options }) => { const api = new GeneratorAPI(id, this, options, rootOptions) apply(api, options, rootOptions, invoking) + + if (apply.hooks) { + apply.hooks(api, options, rootOptions, pluginIds) + } }) + + // load "any" hooks + this.afterAnyInvokeCbs = afterAnyInvokeCbsFromPlugins } async generate ({ @@ -242,12 +281,22 @@ module.exports = class Generator { debug('vue:cli-files')(this.files) } - hasPlugin (_id) { + hasPlugin (_id, _version) { return [ ...this.plugins.map(p => p.id), - ...Object.keys(this.pkg.devDependencies || {}), - ...Object.keys(this.pkg.dependencies || {}) - ].some(id => matchesPluginId(_id, id)) + ...this.allPlugins + ].some(id => { + if (!matchesPluginId(_id, id)) { + return false + } + + if (!_version) { + return true + } + + const version = this.pm.getInstalledVersion(id) + return semver.satisfies(version, _version) + }) } printExitLogs () { diff --git a/packages/@vue/cli/lib/GeneratorAPI.js b/packages/@vue/cli/lib/GeneratorAPI.js index 91cd4d0992..1261676d35 100644 --- a/packages/@vue/cli/lib/GeneratorAPI.js +++ b/packages/@vue/cli/lib/GeneratorAPI.js @@ -133,10 +133,11 @@ class GeneratorAPI { * Check if the project has a given plugin. * * @param {string} id - Plugin id, can omit the (@vue/|vue-|@scope/vue)-cli-plugin- prefix + * @param {string} version - Plugin version. Defaults to '' * @return {boolean} */ - hasPlugin (id) { - return this.generator.hasPlugin(id) + hasPlugin (id, version) { + return this.generator.hasPlugin(id, version) } /** @@ -280,7 +281,21 @@ class GeneratorAPI { * @param {function} cb */ onCreateComplete (cb) { - this.generator.completeCbs.push(cb) + this.afterInvoke(cb) + } + + afterInvoke (cb) { + this.generator.afterInvokeCbs.push(cb) + } + + /** + * Push a callback to be called when the files have been written to disk + * from non invoked plugins + * + * @param {function} cb + */ + afterAnyInvoke (cb) { + this.generator.afterAnyInvokeCbs.push(cb) } /** diff --git a/packages/@vue/cli/lib/add.js b/packages/@vue/cli/lib/add.js index e77e2fd188..251fd9d1ca 100644 --- a/packages/@vue/cli/lib/add.js +++ b/packages/@vue/cli/lib/add.js @@ -5,8 +5,7 @@ const PackageManager = require('./util/ProjectPackageManager') const { log, error, - resolvePluginId, - resolveModule + resolvePluginId } = require('@vue/cli-shared-utils') const confirmIfGitDirty = require('./util/confirmIfGitDirty') @@ -27,12 +26,7 @@ async function add (pluginName, options = {}, context = process.cwd()) { log(`${chalk.green('✔')} Successfully installed plugin: ${chalk.cyan(packageName)}`) log() - const generatorPath = resolveModule(`${packageName}/generator`, context) - if (generatorPath) { - invoke(pluginName, options, context) - } else { - log(`Plugin ${packageName} does not have a generator to invoke`) - } + invoke(pluginName, options, context) } module.exports = (...args) => { diff --git a/packages/@vue/cli/lib/invoke.js b/packages/@vue/cli/lib/invoke.js index 87aeaa9956..7c4ac836fd 100644 --- a/packages/@vue/cli/lib/invoke.js +++ b/packages/@vue/cli/lib/invoke.js @@ -103,12 +103,15 @@ async function invoke (pluginName, options = {}, context = process.cwd()) { async function runGenerator (context, plugin, pkg = getPkg(context)) { const isTestOrDebug = process.env.VUE_CLI_TEST || process.env.VUE_CLI_DEBUG - const createCompleteCbs = [] + const afterInvokeCbs = [] + const afterAnyInvokeCbs = [] + const generator = new Generator(context, { pkg, plugins: [plugin], files: await readFiles(context), - completeCbs: createCompleteCbs, + afterInvokeCbs, + afterAnyInvokeCbs, invoking: true }) @@ -132,9 +135,12 @@ async function runGenerator (context, plugin, pkg = getPkg(context)) { await pm.install() } - if (createCompleteCbs.length) { + if (afterInvokeCbs.length || afterAnyInvokeCbs.length) { logWithSpinner('⚓', `Running completion hooks...`) - for (const cb of createCompleteCbs) { + for (const cb of afterInvokeCbs) { + await cb() + } + for (const cb of afterAnyInvokeCbs) { await cb() } stopSpinner()