Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More flexible hook system #2337

Merged
merged 6 commits into from Jul 21, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/dev-guide/generator-api.md
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
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
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
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
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
@@ -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)
sodatea marked this conversation as resolved.
Show resolved Hide resolved
}
})

// 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
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
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`)
sodatea marked this conversation as resolved.
Show resolved Hide resolved
}
invoke(pluginName, options, context)
}

module.exports = (...args) => {
Expand Down