Skip to content

Commit

Permalink
feat: more flexible hook system for generators (#2337)
Browse files Browse the repository at this point in the history
  • Loading branch information
pksunkara authored and sodatea committed Jul 21, 2019
1 parent 45399b1 commit 544faee
Show file tree
Hide file tree
Showing 9 changed files with 153 additions and 58 deletions.
4 changes: 2 additions & 2 deletions docs/dev-guide/generator-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -177,4 +178,3 @@ Get the entry file taking into account typescript.

- **Usage**:
Checks if the plugin is being invoked.

54 changes: 30 additions & 24 deletions docs/dev-guide/plugin-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 8 additions & 5 deletions packages/@vue/cli-plugin-eslint/generator/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(',')
}
Expand Down Expand Up @@ -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 => {
Expand Down
19 changes: 18 additions & 1 deletion packages/@vue/cli/__tests__/Generator.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
Expand Down
13 changes: 9 additions & 4 deletions packages/@vue/cli/lib/Creator.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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) {
Expand Down Expand Up @@ -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
Expand All @@ -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()
}

Expand Down
63 changes: 56 additions & 7 deletions packages/@vue/cli/lib/Generator.js
Original file line number Diff line number Diff line change
@@ -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')
Expand Down Expand Up @@ -69,17 +71,20 @@ module.exports = class Generator {
constructor (context, {
pkg = {},
plugins = [],
completeCbs = [],
afterInvokeCbs = [],
afterAnyInvokeCbs = [],
files = {},
invoking = false
} = {}) {
this.context = context
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
Expand All @@ -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 ({
Expand Down Expand Up @@ -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 () {
Expand Down
21 changes: 18 additions & 3 deletions packages/@vue/cli/lib/GeneratorAPI.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

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

/**
Expand Down
10 changes: 2 additions & 8 deletions packages/@vue/cli/lib/add.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand All @@ -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) => {
Expand Down
Loading

0 comments on commit 544faee

Please sign in to comment.