Skip to content

Commit 4be4bb3

Browse files
authored
feat: implement plugin execution order (#6411)
1 parent f263a7d commit 4be4bb3

File tree

10 files changed

+372
-28
lines changed

10 files changed

+372
-28
lines changed

Diff for: packages/@vue/cli-plugin-typescript/__tests__/tsGenerator.spec.js

+20-5
Original file line numberDiff line numberDiff line change
@@ -65,19 +65,34 @@ test('use with Babel', async () => {
6565
})
6666

6767
test('use with router', async () => {
68+
const tsApply = require('../generator')
69+
70+
expect(tsApply.after).toBe('@vue/cli-plugin-router')
71+
6872
const { files } = await generateWithPlugin([
6973
{
70-
id: '@vue/cli-plugin-router',
71-
apply: require('@vue/cli-plugin-router/generator'),
74+
id: '@vue/cli-service',
75+
apply: require('@vue/cli-service/generator'),
76+
options: {
77+
plugins: {
78+
'@vue/cli-service': {},
79+
'@vue/cli-plugin-router': {},
80+
'@vue/cli-plugin-typescript': {}
81+
}
82+
}
83+
},
84+
{
85+
id: '@vue/cli-plugin-typescript',
86+
apply: tsApply,
7287
options: {}
7388
},
7489
{
75-
id: 'ts',
76-
apply: require('../generator'),
90+
id: '@vue/cli-plugin-router',
91+
apply: require('@vue/cli-plugin-router/generator'),
7792
options: {}
7893
}
7994
])
80-
expect(files['src/views/Home.vue']).toMatch('<div class="home">')
95+
expect(files['src/views/Home.vue']).toMatch('Welcome to Your Vue.js + TypeScript App')
8196
})
8297

8398
test('tsconfig.json should be valid json', async () => {

Diff for: packages/@vue/cli-plugin-typescript/generator/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,5 @@ module.exports = (
7171

7272
require('./convert')(api, { convertJsToTs })
7373
}
74+
75+
module.exports.after = '@vue/cli-plugin-router'

Diff for: packages/@vue/cli-service/__tests__/Service.spec.js

+47
Original file line numberDiff line numberDiff line change
@@ -381,3 +381,50 @@ test('api: hasPlugin', async () => {
381381
}
382382
])
383383
})
384+
385+
test('order: service plugins order', async () => {
386+
const applyCallOrder = []
387+
function apply (id, order) {
388+
order = order || {}
389+
const fn = jest.fn(() => { applyCallOrder.push(id) })
390+
fn.after = order.after
391+
return fn
392+
}
393+
const service = new Service('/', {
394+
plugins: [
395+
{
396+
id: 'vue-cli-plugin-foo',
397+
apply: apply('vue-cli-plugin-foo')
398+
},
399+
{
400+
id: 'vue-cli-plugin-bar',
401+
apply: apply('vue-cli-plugin-bar', { after: 'vue-cli-plugin-baz' })
402+
},
403+
{
404+
id: 'vue-cli-plugin-baz',
405+
apply: apply('vue-cli-plugin-baz')
406+
}
407+
]
408+
})
409+
expect(service.plugins.map(p => p.id)).toEqual([
410+
'built-in:commands/serve',
411+
'built-in:commands/build',
412+
'built-in:commands/inspect',
413+
'built-in:commands/help',
414+
'built-in:config/base',
415+
'built-in:config/assets',
416+
'built-in:config/css',
417+
'built-in:config/prod',
418+
'built-in:config/app',
419+
'vue-cli-plugin-foo',
420+
'vue-cli-plugin-baz',
421+
'vue-cli-plugin-bar'
422+
])
423+
424+
await service.init()
425+
expect(applyCallOrder).toEqual([
426+
'vue-cli-plugin-foo',
427+
'vue-cli-plugin-baz',
428+
'vue-cli-plugin-bar'
429+
])
430+
})

Diff for: packages/@vue/cli-service/lib/Service.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const PluginAPI = require('./PluginAPI')
66
const dotenv = require('dotenv')
77
const dotenvExpand = require('dotenv-expand')
88
const defaultsDeep = require('lodash.defaultsdeep')
9-
const { warn, error, isPlugin, resolvePluginId, loadModule, resolvePkg, resolveModule } = require('@vue/cli-shared-utils')
9+
const { warn, error, isPlugin, resolvePluginId, loadModule, resolvePkg, resolveModule, sortPlugins } = require('@vue/cli-shared-utils')
1010

1111
const { defaults } = require('./options')
1212
const checkWebpack = require('./util/checkWebpack')
@@ -224,8 +224,12 @@ module.exports = class Service {
224224
apply: loadModule(`./${file}`, this.pkgContext)
225225
})))
226226
}
227+
debug('vue:plugins')(plugins)
227228

228-
return plugins
229+
const orderedPlugins = sortPlugins(plugins)
230+
debug('vue:plugins-ordered')(orderedPlugins)
231+
232+
return orderedPlugins
229233
}
230234

231235
async run (name, args = {}, rawArgv = []) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
const { topologicalSorting } = require('../lib/pluginOrder')
2+
const { logs } = require('../lib/logger')
3+
4+
/**
5+
*
6+
* @param {string} id
7+
* @param {{stage: number, after: string|Array<string>}} [order]
8+
*/
9+
function plugin (id, order) {
10+
order = order || {}
11+
const { after } = order
12+
13+
// use object instead of function here
14+
const apply = {}
15+
apply.after = after
16+
return {
17+
id,
18+
apply
19+
}
20+
}
21+
22+
describe('topologicalSorting', () => {
23+
test(`no specifying 'after' will preserve sort order`, () => {
24+
const plugins = [
25+
plugin('foo'),
26+
plugin('bar'),
27+
plugin('baz')
28+
]
29+
const orderPlugins = topologicalSorting(plugins)
30+
expect(orderPlugins).toEqual(plugins)
31+
})
32+
33+
test(`'after' specified`, () => {
34+
const plugins = [
35+
plugin('foo', { after: 'bar' }),
36+
plugin('bar', { after: 'baz' }),
37+
plugin('baz')
38+
]
39+
const orderPlugins = topologicalSorting(plugins)
40+
expect(orderPlugins).toEqual([
41+
plugin('baz'),
42+
plugin('bar', { after: 'baz' }),
43+
plugin('foo', { after: 'bar' })
44+
])
45+
})
46+
47+
test(`'after' can be Array<string>`, () => {
48+
const plugins = [
49+
plugin('foo', { after: ['bar', 'baz'] }),
50+
plugin('bar'),
51+
plugin('baz')
52+
]
53+
const orderPlugins = topologicalSorting(plugins)
54+
expect(orderPlugins).toEqual([
55+
plugin('bar'),
56+
plugin('baz'),
57+
plugin('foo', { after: ['bar', 'baz'] })
58+
])
59+
})
60+
61+
test('it is not possible to sort plugins because of cyclic graph, return original plugins directly', () => {
62+
logs.warn = []
63+
const plugins = [
64+
plugin('foo', { after: 'bar' }),
65+
plugin('bar', { after: 'baz' }),
66+
plugin('baz', { after: 'foo' })
67+
]
68+
const orderPlugins = topologicalSorting(plugins)
69+
expect(orderPlugins).toEqual(plugins)
70+
71+
expect(logs.warn.length).toBe(1)
72+
})
73+
})

Diff for: packages/@vue/cli-shared-utils/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
'openBrowser',
99
'pkg',
1010
'pluginResolution',
11+
'pluginOrder',
1112
'launch',
1213
'request',
1314
'spinner',

Diff for: packages/@vue/cli-shared-utils/lib/pluginOrder.js

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// @ts-check
2+
const { warn } = require('./logger')
3+
4+
/** @typedef {{after?: string|Array<string>}} Apply */
5+
/** @typedef {{id: string, apply: Apply}} Plugin */
6+
/** @typedef {{after: Set<string>}} OrderParams */
7+
8+
/** @type {Map<string, OrderParams>} */
9+
const orderParamsCache = new Map()
10+
11+
/**
12+
*
13+
* @param {Plugin} plugin
14+
* @returns {OrderParams}
15+
*/
16+
function getOrderParams (plugin) {
17+
if (!process.env.VUE_CLI_TEST && orderParamsCache.has(plugin.id)) {
18+
return orderParamsCache.get(plugin.id)
19+
}
20+
const apply = plugin.apply
21+
22+
let after = new Set()
23+
if (typeof apply.after === 'string') {
24+
after = new Set([apply.after])
25+
} else if (Array.isArray(apply.after)) {
26+
after = new Set(apply.after)
27+
}
28+
if (!process.env.VUE_CLI_TEST) {
29+
orderParamsCache.set(plugin.id, { after })
30+
}
31+
32+
return { after }
33+
}
34+
35+
/**
36+
* See leetcode 210
37+
* @param {Array<Plugin>} plugins
38+
* @returns {Array<Plugin>}
39+
*/
40+
function topologicalSorting (plugins) {
41+
/** @type {Map<string, Plugin>} */
42+
const pluginsMap = new Map(plugins.map(p => [p.id, p]))
43+
44+
/** @type {Map<Plugin, number>} */
45+
const indegrees = new Map()
46+
47+
/** @type {Map<Plugin, Array<Plugin>>} */
48+
const graph = new Map()
49+
50+
plugins.forEach(p => {
51+
const after = getOrderParams(p).after
52+
indegrees.set(p, after.size)
53+
if (after.size === 0) return
54+
for (const id of after) {
55+
const prerequisite = pluginsMap.get(id)
56+
// remove invalid data
57+
if (!prerequisite) {
58+
indegrees.set(p, indegrees.get(p) - 1)
59+
continue
60+
}
61+
62+
if (!graph.has(prerequisite)) {
63+
graph.set(prerequisite, [])
64+
}
65+
graph.get(prerequisite).push(p)
66+
}
67+
})
68+
69+
const res = []
70+
const queue = []
71+
indegrees.forEach((d, p) => {
72+
if (d === 0) queue.push(p)
73+
})
74+
while (queue.length) {
75+
const cur = queue.shift()
76+
res.push(cur)
77+
const neighbors = graph.get(cur)
78+
if (!neighbors) continue
79+
80+
neighbors.forEach(n => {
81+
const degree = indegrees.get(n) - 1
82+
indegrees.set(n, degree)
83+
if (degree === 0) {
84+
queue.push(n)
85+
}
86+
})
87+
}
88+
const valid = res.length === plugins.length
89+
if (!valid) {
90+
warn(`No proper plugin execution order found.`)
91+
return plugins
92+
}
93+
return res
94+
}
95+
96+
/**
97+
* Arrange plugins by 'after' property.
98+
* @param {Array<Plugin>} plugins
99+
* @returns {Array<Plugin>}
100+
*/
101+
function sortPlugins (plugins) {
102+
if (plugins.length < 2) return plugins
103+
104+
return topologicalSorting(plugins)
105+
}
106+
107+
module.exports = {
108+
topologicalSorting,
109+
sortPlugins
110+
}

0 commit comments

Comments
 (0)