diff --git a/packages/@vue/cli-plugin-typescript/__tests__/tsGenerator.spec.js b/packages/@vue/cli-plugin-typescript/__tests__/tsGenerator.spec.js
index a745786439..f2fd642900 100644
--- a/packages/@vue/cli-plugin-typescript/__tests__/tsGenerator.spec.js
+++ b/packages/@vue/cli-plugin-typescript/__tests__/tsGenerator.spec.js
@@ -65,19 +65,34 @@ test('use with Babel', async () => {
})
test('use with router', async () => {
+ const tsApply = require('../generator')
+
+ expect(tsApply.after).toBe('@vue/cli-plugin-router')
+
const { files } = await generateWithPlugin([
{
- id: '@vue/cli-plugin-router',
- apply: require('@vue/cli-plugin-router/generator'),
+ id: '@vue/cli-service',
+ apply: require('@vue/cli-service/generator'),
+ options: {
+ plugins: {
+ '@vue/cli-service': {},
+ '@vue/cli-plugin-router': {},
+ '@vue/cli-plugin-typescript': {}
+ }
+ }
+ },
+ {
+ id: '@vue/cli-plugin-typescript',
+ apply: tsApply,
options: {}
},
{
- id: 'ts',
- apply: require('../generator'),
+ id: '@vue/cli-plugin-router',
+ apply: require('@vue/cli-plugin-router/generator'),
options: {}
}
])
- expect(files['src/views/Home.vue']).toMatch('
')
+ expect(files['src/views/Home.vue']).toMatch('Welcome to Your Vue.js + TypeScript App')
})
test('tsconfig.json should be valid json', async () => {
diff --git a/packages/@vue/cli-plugin-typescript/generator/index.js b/packages/@vue/cli-plugin-typescript/generator/index.js
index dc776d10b8..2c86584e18 100644
--- a/packages/@vue/cli-plugin-typescript/generator/index.js
+++ b/packages/@vue/cli-plugin-typescript/generator/index.js
@@ -71,3 +71,5 @@ module.exports = (
require('./convert')(api, { convertJsToTs })
}
+
+module.exports.after = '@vue/cli-plugin-router'
diff --git a/packages/@vue/cli-service/__tests__/Service.spec.js b/packages/@vue/cli-service/__tests__/Service.spec.js
index 6fca99358a..b593b7936c 100644
--- a/packages/@vue/cli-service/__tests__/Service.spec.js
+++ b/packages/@vue/cli-service/__tests__/Service.spec.js
@@ -381,3 +381,50 @@ test('api: hasPlugin', async () => {
}
])
})
+
+test('order: service plugins order', async () => {
+ const applyCallOrder = []
+ function apply (id, order) {
+ order = order || {}
+ const fn = jest.fn(() => { applyCallOrder.push(id) })
+ fn.after = order.after
+ return fn
+ }
+ const service = new Service('/', {
+ plugins: [
+ {
+ id: 'vue-cli-plugin-foo',
+ apply: apply('vue-cli-plugin-foo')
+ },
+ {
+ id: 'vue-cli-plugin-bar',
+ apply: apply('vue-cli-plugin-bar', { after: 'vue-cli-plugin-baz' })
+ },
+ {
+ id: 'vue-cli-plugin-baz',
+ apply: apply('vue-cli-plugin-baz')
+ }
+ ]
+ })
+ expect(service.plugins.map(p => p.id)).toEqual([
+ 'built-in:commands/serve',
+ 'built-in:commands/build',
+ 'built-in:commands/inspect',
+ 'built-in:commands/help',
+ 'built-in:config/base',
+ 'built-in:config/assets',
+ 'built-in:config/css',
+ 'built-in:config/prod',
+ 'built-in:config/app',
+ 'vue-cli-plugin-foo',
+ 'vue-cli-plugin-baz',
+ 'vue-cli-plugin-bar'
+ ])
+
+ await service.init()
+ expect(applyCallOrder).toEqual([
+ 'vue-cli-plugin-foo',
+ 'vue-cli-plugin-baz',
+ 'vue-cli-plugin-bar'
+ ])
+})
diff --git a/packages/@vue/cli-service/lib/Service.js b/packages/@vue/cli-service/lib/Service.js
index 5974a8cd1a..82d6c58aaa 100644
--- a/packages/@vue/cli-service/lib/Service.js
+++ b/packages/@vue/cli-service/lib/Service.js
@@ -6,7 +6,7 @@ const PluginAPI = require('./PluginAPI')
const dotenv = require('dotenv')
const dotenvExpand = require('dotenv-expand')
const defaultsDeep = require('lodash.defaultsdeep')
-const { warn, error, isPlugin, resolvePluginId, loadModule, resolvePkg, resolveModule } = require('@vue/cli-shared-utils')
+const { warn, error, isPlugin, resolvePluginId, loadModule, resolvePkg, resolveModule, sortPlugins } = require('@vue/cli-shared-utils')
const { defaults } = require('./options')
const checkWebpack = require('./util/checkWebpack')
@@ -224,8 +224,12 @@ module.exports = class Service {
apply: loadModule(`./${file}`, this.pkgContext)
})))
}
+ debug('vue:plugins')(plugins)
- return plugins
+ const orderedPlugins = sortPlugins(plugins)
+ debug('vue:plugins-ordered')(orderedPlugins)
+
+ return orderedPlugins
}
async run (name, args = {}, rawArgv = []) {
diff --git a/packages/@vue/cli-shared-utils/__tests__/pluginOrder.spec.js b/packages/@vue/cli-shared-utils/__tests__/pluginOrder.spec.js
new file mode 100644
index 0000000000..0adc0638b9
--- /dev/null
+++ b/packages/@vue/cli-shared-utils/__tests__/pluginOrder.spec.js
@@ -0,0 +1,73 @@
+const { topologicalSorting } = require('../lib/pluginOrder')
+const { logs } = require('../lib/logger')
+
+/**
+ *
+ * @param {string} id
+ * @param {{stage: number, after: string|Array
}} [order]
+ */
+function plugin (id, order) {
+ order = order || {}
+ const { after } = order
+
+ // use object instead of function here
+ const apply = {}
+ apply.after = after
+ return {
+ id,
+ apply
+ }
+}
+
+describe('topologicalSorting', () => {
+ test(`no specifying 'after' will preserve sort order`, () => {
+ const plugins = [
+ plugin('foo'),
+ plugin('bar'),
+ plugin('baz')
+ ]
+ const orderPlugins = topologicalSorting(plugins)
+ expect(orderPlugins).toEqual(plugins)
+ })
+
+ test(`'after' specified`, () => {
+ const plugins = [
+ plugin('foo', { after: 'bar' }),
+ plugin('bar', { after: 'baz' }),
+ plugin('baz')
+ ]
+ const orderPlugins = topologicalSorting(plugins)
+ expect(orderPlugins).toEqual([
+ plugin('baz'),
+ plugin('bar', { after: 'baz' }),
+ plugin('foo', { after: 'bar' })
+ ])
+ })
+
+ test(`'after' can be Array`, () => {
+ const plugins = [
+ plugin('foo', { after: ['bar', 'baz'] }),
+ plugin('bar'),
+ plugin('baz')
+ ]
+ const orderPlugins = topologicalSorting(plugins)
+ expect(orderPlugins).toEqual([
+ plugin('bar'),
+ plugin('baz'),
+ plugin('foo', { after: ['bar', 'baz'] })
+ ])
+ })
+
+ test('it is not possible to sort plugins because of cyclic graph, return original plugins directly', () => {
+ logs.warn = []
+ const plugins = [
+ plugin('foo', { after: 'bar' }),
+ plugin('bar', { after: 'baz' }),
+ plugin('baz', { after: 'foo' })
+ ]
+ const orderPlugins = topologicalSorting(plugins)
+ expect(orderPlugins).toEqual(plugins)
+
+ expect(logs.warn.length).toBe(1)
+ })
+})
diff --git a/packages/@vue/cli-shared-utils/index.js b/packages/@vue/cli-shared-utils/index.js
index 830a325459..270ca0e579 100644
--- a/packages/@vue/cli-shared-utils/index.js
+++ b/packages/@vue/cli-shared-utils/index.js
@@ -8,6 +8,7 @@
'openBrowser',
'pkg',
'pluginResolution',
+ 'pluginOrder',
'launch',
'request',
'spinner',
diff --git a/packages/@vue/cli-shared-utils/lib/pluginOrder.js b/packages/@vue/cli-shared-utils/lib/pluginOrder.js
new file mode 100644
index 0000000000..8d1043cd4f
--- /dev/null
+++ b/packages/@vue/cli-shared-utils/lib/pluginOrder.js
@@ -0,0 +1,110 @@
+// @ts-check
+const { warn } = require('./logger')
+
+/** @typedef {{after?: string|Array}} Apply */
+/** @typedef {{id: string, apply: Apply}} Plugin */
+/** @typedef {{after: Set}} OrderParams */
+
+/** @type {Map} */
+const orderParamsCache = new Map()
+
+/**
+ *
+ * @param {Plugin} plugin
+ * @returns {OrderParams}
+ */
+function getOrderParams (plugin) {
+ if (!process.env.VUE_CLI_TEST && orderParamsCache.has(plugin.id)) {
+ return orderParamsCache.get(plugin.id)
+ }
+ const apply = plugin.apply
+
+ let after = new Set()
+ if (typeof apply.after === 'string') {
+ after = new Set([apply.after])
+ } else if (Array.isArray(apply.after)) {
+ after = new Set(apply.after)
+ }
+ if (!process.env.VUE_CLI_TEST) {
+ orderParamsCache.set(plugin.id, { after })
+ }
+
+ return { after }
+}
+
+/**
+ * See leetcode 210
+ * @param {Array} plugins
+ * @returns {Array}
+ */
+function topologicalSorting (plugins) {
+ /** @type {Map} */
+ const pluginsMap = new Map(plugins.map(p => [p.id, p]))
+
+ /** @type {Map} */
+ const indegrees = new Map()
+
+ /** @type {Map>} */
+ const graph = new Map()
+
+ plugins.forEach(p => {
+ const after = getOrderParams(p).after
+ indegrees.set(p, after.size)
+ if (after.size === 0) return
+ for (const id of after) {
+ const prerequisite = pluginsMap.get(id)
+ // remove invalid data
+ if (!prerequisite) {
+ indegrees.set(p, indegrees.get(p) - 1)
+ continue
+ }
+
+ if (!graph.has(prerequisite)) {
+ graph.set(prerequisite, [])
+ }
+ graph.get(prerequisite).push(p)
+ }
+ })
+
+ const res = []
+ const queue = []
+ indegrees.forEach((d, p) => {
+ if (d === 0) queue.push(p)
+ })
+ while (queue.length) {
+ const cur = queue.shift()
+ res.push(cur)
+ const neighbors = graph.get(cur)
+ if (!neighbors) continue
+
+ neighbors.forEach(n => {
+ const degree = indegrees.get(n) - 1
+ indegrees.set(n, degree)
+ if (degree === 0) {
+ queue.push(n)
+ }
+ })
+ }
+ const valid = res.length === plugins.length
+ if (!valid) {
+ warn(`No proper plugin execution order found.`)
+ return plugins
+ }
+ return res
+}
+
+/**
+ * Arrange plugins by 'after' property.
+ * @param {Array} plugins
+ * @returns {Array}
+ */
+function sortPlugins (plugins) {
+ if (plugins.length < 2) return plugins
+
+ return topologicalSorting(plugins)
+}
+
+module.exports = {
+ topologicalSorting,
+ sortPlugins
+}
diff --git a/packages/@vue/cli/__tests__/Generator.spec.js b/packages/@vue/cli/__tests__/Generator.spec.js
index a7f4334574..0183d9f54e 100644
--- a/packages/@vue/cli/__tests__/Generator.spec.js
+++ b/packages/@vue/cli/__tests__/Generator.spec.js
@@ -1167,3 +1167,92 @@ test('run a codemod on the entry file', async () => {
await generator.generate()
expect(fs.readFileSync('/main.js', 'utf-8')).toMatch(/new TestVue/)
})
+
+test('order: generator plugins order', async () => {
+ const applyCallOrder = []
+ function apply (id, order) {
+ order = order || {}
+ const fn = jest.fn(() => { applyCallOrder.push(id) })
+ fn.after = order.after
+ return fn
+ }
+ const generator = new Generator('/', {
+ plugins: [
+ {
+ id: 'vue-cli-plugin-foo',
+ apply: apply('vue-cli-plugin-foo')
+ },
+ {
+ id: 'vue-cli-plugin-bar',
+ apply: apply('vue-cli-plugin-bar', { after: 'vue-cli-plugin-baz' })
+ },
+ {
+ id: 'vue-cli-plugin-baz',
+ apply: apply('vue-cli-plugin-baz')
+ }
+ ]
+ })
+ await generator.generate()
+
+ expect(applyCallOrder).toEqual([
+ 'vue-cli-plugin-foo',
+ 'vue-cli-plugin-baz',
+ 'vue-cli-plugin-bar'
+ ])
+})
+
+test('order: afterAnyInvoke order', async () => {
+ const fooAnyInvokeHandler = () => {}
+ const barAnyInvokeHandler = () => {}
+ const bazAnyInvokeHandler = () => {}
+
+ const getGeneratorFn = (anyInvokeHandler, order) => {
+ order = order || {}
+ const generatorFn = () => {}
+ generatorFn.hooks = api => {
+ api.afterAnyInvoke(anyInvokeHandler)
+ }
+ generatorFn.after = order.after
+ return generatorFn
+ }
+
+ jest.doMock('vue-cli-plugin-foo-order/generator', () => {
+ return getGeneratorFn(fooAnyInvokeHandler, { after: 'vue-cli-plugin-bar-order' })
+ }, { virtual: true })
+
+ jest.doMock('vue-cli-plugin-bar-order/generator', () => {
+ return getGeneratorFn(barAnyInvokeHandler)
+ }, { virtual: true })
+
+ jest.doMock('vue-cli-plugin-baz-order/generator', () => {
+ return getGeneratorFn(bazAnyInvokeHandler)
+ }, { virtual: true })
+
+ const afterAnyInvokeCbs = []
+ const afterInvokeCbs = []
+ const generator = new Generator('/', {
+ pkg: {
+ devDependencies: {
+ 'vue-cli-plugin-foo-order': '1.0.0',
+ 'vue-cli-plugin-bar-order': '1.0.0',
+ 'vue-cli-plugin-baz-order': '1.0.0'
+ }
+ },
+ plugins: [
+ {
+ id: 'vue-cli-plugin-foo-order',
+ apply: getGeneratorFn(fooAnyInvokeHandler)
+ }
+ ],
+ afterInvokeCbs,
+ afterAnyInvokeCbs
+ })
+
+ await generator.generate()
+
+ expect(afterAnyInvokeCbs).toEqual([
+ barAnyInvokeHandler,
+ bazAnyInvokeHandler,
+ fooAnyInvokeHandler
+ ])
+})
diff --git a/packages/@vue/cli/lib/Creator.js b/packages/@vue/cli/lib/Creator.js
index 925bdb9f6d..4931cbf869 100644
--- a/packages/@vue/cli/lib/Creator.js
+++ b/packages/@vue/cli/lib/Creator.js
@@ -111,16 +111,6 @@ module.exports = class Creator extends EventEmitter {
}
}
- // Introducing this hack because typescript plugin must be invoked after router.
- // Currently we rely on the `plugins` object enumeration order,
- // which depends on the order of the field initialization.
- // FIXME: Remove this ugly hack after the plugin ordering API settled down
- if (preset.plugins['@vue/cli-plugin-router'] && preset.plugins['@vue/cli-plugin-typescript']) {
- const tmp = preset.plugins['@vue/cli-plugin-typescript']
- delete preset.plugins['@vue/cli-plugin-typescript']
- preset.plugins['@vue/cli-plugin-typescript'] = tmp
- }
-
// legacy support for vuex
if (preset.vuex) {
preset.plugins['@vue/cli-plugin-vuex'] = {}
diff --git a/packages/@vue/cli/lib/Generator.js b/packages/@vue/cli/lib/Generator.js
index c8f1a22c5c..5b26ee78cc 100644
--- a/packages/@vue/cli/lib/Generator.js
+++ b/packages/@vue/cli/lib/Generator.js
@@ -14,7 +14,9 @@ const {
toShortPluginId,
matchesPluginId,
- loadModule
+ loadModule,
+
+ sortPlugins
} = require('@vue/cli-shared-utils')
const ConfigTransform = require('./ConfigTransform')
@@ -109,7 +111,7 @@ module.exports = class Generator {
invoking = false
} = {}) {
this.context = context
- this.plugins = plugins
+ this.plugins = sortPlugins(plugins)
this.originalPkg = pkg
this.pkg = Object.assign({}, pkg)
this.pm = new PackageManager({ context })
@@ -135,9 +137,7 @@ module.exports = class Generator {
this.exitLogs = []
// load all the other plugins
- this.allPluginIds = Object.keys(this.pkg.dependencies || {})
- .concat(Object.keys(this.pkg.devDependencies || {}))
- .filter(isPlugin)
+ this.allPlugins = this.resolveAllPlugins()
const cliService = plugins.find(p => p.id === '@vue/cli-service')
const rootOptions = cliService
@@ -155,12 +155,12 @@ module.exports = class Generator {
const passedAfterInvokeCbs = this.afterInvokeCbs
this.afterInvokeCbs = []
// apply hooks from all plugins to collect 'afterAnyHooks'
- for (const id of this.allPluginIds) {
+ for (const plugin of this.allPlugins) {
+ const { id, apply } = plugin
const api = new GeneratorAPI(id, this, {}, rootOptions)
- const pluginGenerator = loadModule(`${id}/generator`, this.context)
- if (pluginGenerator && pluginGenerator.hooks) {
- await pluginGenerator.hooks(api, {}, rootOptions, pluginIds)
+ if (apply.hooks) {
+ await apply.hooks(api, {}, rootOptions, pluginIds)
}
}
@@ -182,7 +182,7 @@ module.exports = class Generator {
if (apply.hooks) {
// while we execute the entire `hooks` function,
// only the `afterInvoke` hook is respected
- // because `afterAnyHooks` is already determined by the `allPluginIds` loop above
+ // because `afterAnyHooks` is already determined by the `allPlugins` loop above
await apply.hooks(api, options, rootOptions, pluginIds)
}
}
@@ -291,6 +291,19 @@ module.exports = class Generator {
debug('vue:cli-pkg')(this.pkg)
}
+ resolveAllPlugins () {
+ const allPlugins = []
+ Object.keys(this.pkg.dependencies || {})
+ .concat(Object.keys(this.pkg.devDependencies || {}))
+ .forEach(id => {
+ if (!isPlugin(id)) return
+ const pluginGenerator = loadModule(`${id}/generator`, this.context)
+ if (!pluginGenerator) return
+ allPlugins.push({ id, apply: pluginGenerator })
+ })
+ return sortPlugins(allPlugins)
+ }
+
async resolveFiles () {
const files = this.files
for (const middleware of this.fileMiddlewares) {
@@ -333,7 +346,7 @@ module.exports = class Generator {
hasPlugin (id, versionRange) {
const pluginExists = [
...this.plugins.map(p => p.id),
- ...this.allPluginIds
+ ...this.allPlugins.map(p => p.id)
].some(pid => matchesPluginId(id, pid))
if (!pluginExists) {