mirror of
https://github.com/vuejs/vue-cli.git
synced 2026-03-13 04:31:08 -05:00
feat: more flexible hook system for generators (#2337)
This commit is contained in:
committed by
Haoqun Jiang
parent
45399b192a
commit
544faee81e
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user