Skip to content

Commit 544faee

Browse files
pksunkarahaoqunjiang
authored andcommitted
feat: more flexible hook system for generators (#2337)
1 parent 45399b1 commit 544faee

File tree

9 files changed

+153
-58
lines changed

9 files changed

+153
-58
lines changed

docs/dev-guide/generator-api.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,13 @@ Resolve a path for the current project
5454

5555
- **Arguments**
5656
- `{string} id` - plugin id, can omit the (@vue/|vue-|@scope/vue)-cli-plugin- prefix
57+
- `{string} version` - semver version range, optional
5758

5859
- **Returns**
5960
- `{boolean}`
6061

6162
- **Usage**:
62-
Check if the project has a plugin with given id
63+
Check if the project has a plugin with given id. If version range is given, then the plugin version should satisfy it
6364

6465
## addConfigTransform
6566

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

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

docs/dev-guide/plugin-dev.md

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -241,51 +241,57 @@ Let's consider the case where we have created a `router.js` file via [templating
241241
api.injectImports(api.entryFile, `import router from './router'`)
242242
```
243243

244-
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.
244+
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.
245245

246246
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:
247247

248248
```js
249249
// generator/index.js
250250

251-
api.onCreateComplete(() => {
252-
const fs = require('fs')
253-
const contentMain = fs.readFileSync(api.entryFile, { encoding: 'utf-8' })
254-
const lines = contentMain.split(/\r?\n/g)
255-
})
251+
module.exports.hooks = (api) => {
252+
api.afterInvoke(() => {
253+
const fs = require('fs')
254+
const contentMain = fs.readFileSync(api.entryFile, { encoding: 'utf-8' })
255+
const lines = contentMain.split(/\r?\n/g)
256+
})
257+
}
256258
```
257259

258260
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:
259261

260-
```js{8-9}
262+
```js{9-10}
261263
// generator/index.js
262264
263-
api.onCreateComplete(() => {
264-
const fs = require('fs')
265-
const contentMain = fs.readFileSync(api.entryFile, { encoding: 'utf-8' })
266-
const lines = contentMain.split(/\r?\n/g)
265+
module.exports.hooks = (api) => {
266+
api.afterInvoke(() => {
267+
const fs = require('fs')
268+
const contentMain = fs.readFileSync(api.entryFile, { encoding: 'utf-8' })
269+
const lines = contentMain.split(/\r?\n/g)
267270
268-
const renderIndex = lines.findIndex(line => line.match(/render/))
269-
lines[renderIndex] += `\n router,`
270-
})
271+
const renderIndex = lines.findIndex(line => line.match(/render/))
272+
lines[renderIndex] += `\n router,`
273+
})
274+
}
271275
```
272276

273277
Finally, you need to write the content back to the main file:
274278

275-
```js{2,11}
279+
```js{12-13}
276280
// generator/index.js
277281
278-
api.onCreateComplete(() => {
279-
const { EOL } = require('os')
280-
const fs = require('fs')
281-
const contentMain = fs.readFileSync(api.entryFile, { encoding: 'utf-8' })
282-
const lines = contentMain.split(/\r?\n/g)
282+
module.exports.hooks = (api) => {
283+
api.afterInvoke(() => {
284+
const { EOL } = require('os')
285+
const fs = require('fs')
286+
const contentMain = fs.readFileSync(api.entryFile, { encoding: 'utf-8' })
287+
const lines = contentMain.split(/\r?\n/g)
283288
284-
const renderIndex = lines.findIndex(line => line.match(/render/))
285-
lines[renderIndex] += `${EOL} router,`
289+
const renderIndex = lines.findIndex(line => line.match(/render/))
290+
lines[renderIndex] += `${EOL} router,`
286291
287-
fs.writeFileSync(api.entryFile, lines.join(EOL), { encoding: 'utf-8' })
288-
})
292+
fs.writeFileSync(api.entryFile, lines.join(EOL), { encoding: 'utf-8' })
293+
})
294+
}
289295
```
290296

291297
## Service Plugin

packages/@vue/cli-plugin-eslint/generator/index.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ const fs = require('fs')
22
const path = require('path')
33

44
module.exports = (api, { config, lintOn = [] }, _, invoking) => {
5+
api.assertCliVersion('^4.0.0-alpha.4')
6+
api.assertCliServiceVersion('^4.0.0-alpha.4')
7+
58
if (typeof lintOn === 'string') {
69
lintOn = lintOn.split(',')
710
}
@@ -97,13 +100,13 @@ module.exports = (api, { config, lintOn = [] }, _, invoking) => {
97100
require('@vue/cli-plugin-unit-jest/generator').applyESLint(api)
98101
}
99102
}
103+
}
100104

105+
module.exports.hooks = (api) => {
101106
// lint & fix after create to ensure files adhere to chosen config
102-
if (config && config !== 'base') {
103-
api.onCreateComplete(() => {
104-
require('../lint')({ silent: true }, api)
105-
})
106-
}
107+
api.afterAnyInvoke(() => {
108+
require('../lint')({ silent: true }, api)
109+
})
107110
}
108111

109112
const applyTS = module.exports.applyTS = api => {

packages/@vue/cli/__tests__/Generator.spec.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,24 @@ test('api: onCreateComplete', () => {
448448
}
449449
}
450450
],
451-
completeCbs: cbs
451+
afterInvokeCbs: cbs
452+
})
453+
expect(cbs).toContain(fn)
454+
})
455+
456+
test('api: afterInvoke', () => {
457+
const fn = () => {}
458+
const cbs = []
459+
new Generator('/', {
460+
plugins: [
461+
{
462+
id: 'test',
463+
apply: api => {
464+
api.afterInvoke(fn)
465+
}
466+
}
467+
],
468+
afterInvokeCbs: cbs
452469
})
453470
expect(cbs).toContain(fn)
454471
})

packages/@vue/cli/lib/Creator.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ module.exports = class Creator extends EventEmitter {
5454
this.outroPrompts = this.resolveOutroPrompts()
5555
this.injectedPrompts = []
5656
this.promptCompleteCbs = []
57-
this.createCompleteCbs = []
57+
this.afterInvokeCbs = []
58+
this.afterAnyInvokeCbs = []
5859

5960
this.run = this.run.bind(this)
6061

@@ -64,7 +65,7 @@ module.exports = class Creator extends EventEmitter {
6465

6566
async create (cliOptions = {}, preset = null) {
6667
const isTestOrDebug = process.env.VUE_CLI_TEST || process.env.VUE_CLI_DEBUG
67-
const { run, name, context, createCompleteCbs } = this
68+
const { run, name, context, afterInvokeCbs, afterAnyInvokeCbs } = this
6869

6970
if (!preset) {
7071
if (cliOptions.preset) {
@@ -187,7 +188,8 @@ module.exports = class Creator extends EventEmitter {
187188
const generator = new Generator(context, {
188189
pkg,
189190
plugins,
190-
completeCbs: createCompleteCbs
191+
afterInvokeCbs,
192+
afterAnyInvokeCbs
191193
})
192194
await generator.generate({
193195
extractConfigFiles: preset.useConfigFiles
@@ -204,7 +206,10 @@ module.exports = class Creator extends EventEmitter {
204206
// run complete cbs if any (injected by generators)
205207
logWithSpinner('⚓', `Running completion hooks...`)
206208
this.emit('creation', { event: 'completion-hooks' })
207-
for (const cb of createCompleteCbs) {
209+
for (const cb of afterInvokeCbs) {
210+
await cb()
211+
}
212+
for (const cb of afterAnyInvokeCbs) {
208213
await cb()
209214
}
210215

packages/@vue/cli/lib/Generator.js

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
const ejs = require('ejs')
22
const debug = require('debug')
3+
const semver = require('semver')
34
const GeneratorAPI = require('./GeneratorAPI')
5+
const PackageManager = require('./util/ProjectPackageManager')
46
const sortObject = require('./util/sortObject')
57
const writeFileTree = require('./util/writeFileTree')
68
const inferRootOptions = require('./util/inferRootOptions')
79
const normalizeFilePaths = require('./util/normalizeFilePaths')
810
const runCodemod = require('./util/runCodemod')
9-
const { toShortPluginId, matchesPluginId } = require('@vue/cli-shared-utils')
11+
const { toShortPluginId, matchesPluginId, loadModule, isPlugin } = require('@vue/cli-shared-utils')
1012
const ConfigTransform = require('./ConfigTransform')
1113

1214
const logger = require('@vue/cli-shared-utils/lib/logger')
@@ -69,17 +71,20 @@ module.exports = class Generator {
6971
constructor (context, {
7072
pkg = {},
7173
plugins = [],
72-
completeCbs = [],
74+
afterInvokeCbs = [],
75+
afterAnyInvokeCbs = [],
7376
files = {},
7477
invoking = false
7578
} = {}) {
7679
this.context = context
7780
this.plugins = plugins
7881
this.originalPkg = pkg
7982
this.pkg = Object.assign({}, pkg)
83+
this.pm = new PackageManager({ context })
8084
this.imports = {}
8185
this.rootOptions = {}
82-
this.completeCbs = completeCbs
86+
this.afterInvokeCbs = []
87+
this.afterAnyInvokeCbs = afterAnyInvokeCbs
8388
this.configTransforms = {}
8489
this.defaultConfigTransforms = defaultConfigTransforms
8590
this.reservedConfigTransforms = reservedConfigTransforms
@@ -93,15 +98,49 @@ module.exports = class Generator {
9398
// exit messages
9499
this.exitLogs = []
95100

101+
const pluginIds = plugins.map(p => p.id)
102+
103+
// load all the other plugins
104+
this.allPlugins = Object.keys(this.pkg.dependencies || {})
105+
.concat(Object.keys(this.pkg.devDependencies || {}))
106+
.filter(isPlugin)
107+
96108
const cliService = plugins.find(p => p.id === '@vue/cli-service')
97109
const rootOptions = cliService
98110
? cliService.options
99111
: inferRootOptions(pkg)
112+
113+
// apply hooks from all plugins
114+
this.allPlugins.forEach(id => {
115+
const api = new GeneratorAPI(id, this, {}, rootOptions)
116+
const pluginGenerator = loadModule(`${id}/generator`, context)
117+
118+
if (pluginGenerator && pluginGenerator.hooks) {
119+
pluginGenerator.hooks(api, {}, rootOptions, pluginIds)
120+
}
121+
})
122+
123+
// We are doing save/load to make the hook order deterministic
124+
// save "any" hooks
125+
const afterAnyInvokeCbsFromPlugins = this.afterAnyInvokeCbs
126+
127+
// reset hooks
128+
this.afterInvokeCbs = afterInvokeCbs
129+
this.afterAnyInvokeCbs = []
130+
this.postProcessFilesCbs = []
131+
100132
// apply generators from plugins
101133
plugins.forEach(({ id, apply, options }) => {
102134
const api = new GeneratorAPI(id, this, options, rootOptions)
103135
apply(api, options, rootOptions, invoking)
136+
137+
if (apply.hooks) {
138+
apply.hooks(api, options, rootOptions, pluginIds)
139+
}
104140
})
141+
142+
// load "any" hooks
143+
this.afterAnyInvokeCbs = afterAnyInvokeCbsFromPlugins
105144
}
106145

107146
async generate ({
@@ -242,12 +281,22 @@ module.exports = class Generator {
242281
debug('vue:cli-files')(this.files)
243282
}
244283

245-
hasPlugin (_id) {
284+
hasPlugin (_id, _version) {
246285
return [
247286
...this.plugins.map(p => p.id),
248-
...Object.keys(this.pkg.devDependencies || {}),
249-
...Object.keys(this.pkg.dependencies || {})
250-
].some(id => matchesPluginId(_id, id))
287+
...this.allPlugins
288+
].some(id => {
289+
if (!matchesPluginId(_id, id)) {
290+
return false
291+
}
292+
293+
if (!_version) {
294+
return true
295+
}
296+
297+
const version = this.pm.getInstalledVersion(id)
298+
return semver.satisfies(version, _version)
299+
})
251300
}
252301

253302
printExitLogs () {

packages/@vue/cli/lib/GeneratorAPI.js

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,10 +133,11 @@ class GeneratorAPI {
133133
* Check if the project has a given plugin.
134134
*
135135
* @param {string} id - Plugin id, can omit the (@vue/|vue-|@scope/vue)-cli-plugin- prefix
136+
* @param {string} version - Plugin version. Defaults to ''
136137
* @return {boolean}
137138
*/
138-
hasPlugin (id) {
139-
return this.generator.hasPlugin(id)
139+
hasPlugin (id, version) {
140+
return this.generator.hasPlugin(id, version)
140141
}
141142

142143
/**
@@ -280,7 +281,21 @@ class GeneratorAPI {
280281
* @param {function} cb
281282
*/
282283
onCreateComplete (cb) {
283-
this.generator.completeCbs.push(cb)
284+
this.afterInvoke(cb)
285+
}
286+
287+
afterInvoke (cb) {
288+
this.generator.afterInvokeCbs.push(cb)
289+
}
290+
291+
/**
292+
* Push a callback to be called when the files have been written to disk
293+
* from non invoked plugins
294+
*
295+
* @param {function} cb
296+
*/
297+
afterAnyInvoke (cb) {
298+
this.generator.afterAnyInvokeCbs.push(cb)
284299
}
285300

286301
/**

packages/@vue/cli/lib/add.js

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ const PackageManager = require('./util/ProjectPackageManager')
55
const {
66
log,
77
error,
8-
resolvePluginId,
9-
resolveModule
8+
resolvePluginId
109
} = require('@vue/cli-shared-utils')
1110
const confirmIfGitDirty = require('./util/confirmIfGitDirty')
1211

@@ -27,12 +26,7 @@ async function add (pluginName, options = {}, context = process.cwd()) {
2726
log(`${chalk.green('✔')} Successfully installed plugin: ${chalk.cyan(packageName)}`)
2827
log()
2928

30-
const generatorPath = resolveModule(`${packageName}/generator`, context)
31-
if (generatorPath) {
32-
invoke(pluginName, options, context)
33-
} else {
34-
log(`Plugin ${packageName} does not have a generator to invoke`)
35-
}
29+
invoke(pluginName, options, context)
3630
}
3731

3832
module.exports = (...args) => {

0 commit comments

Comments
 (0)