feat: more flexible hook system for generators (#2337)

This commit is contained in:
Pavan Kumar Sunkara
2019-07-21 13:59:18 +02:00
committed by Haoqun Jiang
parent 45399b192a
commit 544faee81e
9 changed files with 153 additions and 58 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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 => {

View File

@@ -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)
})

View File

@@ -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()
}

View File

@@ -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 () {

View File

@@ -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)
}
/**

View File

@@ -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) => {

View File

@@ -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()