feat!: make router a separate plugin (#4196)

* refactor: move router to its own plugin

* refactor: rename routerHistoryMode option to historyMode

* test: add @vue/cli-plugin-router tests

* feat: change src/router.js for most common use cases

* fix: fix cli-ui tests

* docs: Remove router root option from docs

* fix: add support for legacy router option
This commit is contained in:
Pavan Kumar Sunkara
2019-07-05 17:21:29 +02:00
committed by Haoqun Jiang
parent 9eadfe1eba
commit 246ae678cb
33 changed files with 305 additions and 187 deletions

View File

@@ -0,0 +1,2 @@
__tests__
__mocks__

View File

@@ -0,0 +1,9 @@
# @vue/cli-plugin-router
> router plugin for vue-cli
## Installing in an Already Created Project
``` sh
vue add @vue/router
```

View File

@@ -0,0 +1,64 @@
const generateWithPlugin = require('@vue/cli-test-utils/generateWithPlugin')
test('base', async () => {
const { files, pkg } = await generateWithPlugin({
id: 'router',
apply: require('../generator'),
options: {}
})
expect(files['src/router/index.js']).toBeTruthy()
expect(files['src/router/index.js']).not.toMatch('history')
expect(files['src/views/About.vue']).toBeTruthy()
expect(files['src/views/Home.vue']).toBeTruthy()
expect(files['src/App.vue']).toMatch('<router-link to="/">Home</router-link>')
expect(files['src/App.vue']).not.toMatch('<script>')
expect(files['src/App.vue']).toMatch('#nav a.router-link-exact-active')
expect(pkg.dependencies).toHaveProperty('vue-router')
})
test('history mode', async () => {
const { files, pkg } = await generateWithPlugin({
id: 'router',
apply: require('../generator'),
options: {
historyMode: true
}
})
expect(files['src/router/index.js']).toBeTruthy()
expect(files['src/router/index.js']).toMatch('history')
expect(files['src/views/About.vue']).toBeTruthy()
expect(files['src/views/Home.vue']).toBeTruthy()
expect(files['src/App.vue']).toMatch('<router-link to="/">Home</router-link>')
expect(files['src/App.vue']).not.toMatch('<script>')
expect(files['src/App.vue']).toMatch('#nav a.router-link-exact-active')
expect(pkg.dependencies).toHaveProperty('vue-router')
})
test('use with Babel', async () => {
const { pkg, files } = await generateWithPlugin([
{
id: 'babel',
apply: require('@vue/cli-plugin-babel/generator'),
options: {}
},
{
id: 'router',
apply: require('../generator'),
options: {}
}
])
expect(files['src/router/index.js']).toBeTruthy()
expect(files['src/router/index.js']).toMatch('component: () => import')
expect(files['src/views/About.vue']).toBeTruthy()
expect(files['src/views/Home.vue']).toBeTruthy()
expect(files['src/App.vue']).toMatch('<router-link to="/">Home</router-link>')
expect(files['src/App.vue']).not.toMatch('<script>')
expect(files['src/App.vue']).toMatch('#nav a.router-link-exact-active')
expect(pkg.dependencies).toHaveProperty('vue-router')
})

View File

@@ -1,34 +1,22 @@
module.exports = (api, options = {}) => {
api.assertCliVersion('^4.0.0-alpha.3')
api.assertCliServiceVersion('^4.0.0-alpha.3')
api.injectImports(api.entryFile, `import router from './router'`)
api.injectRootOptions(api.entryFile, `router`)
api.extendPackage({
dependencies: {
'vue-router': '^3.0.6'
}
})
api.render('./template', {
historyMode: options.routerHistoryMode,
historyMode: options.historyMode,
doesCompile: api.hasPlugin('babel') || api.hasPlugin('typescript')
})
if (api.invoking) {
api.postProcessFiles(files => {
const appFile = files[`src/App.vue`]
if (appFile) {
files[`src/App.vue`] = appFile.replace(/^<template>[^]+<\/script>/, `
<template>
<div id="app">
<div id="nav">
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</div>
<router-view/>
</div>
</template>
`.trim())
}
})
if (api.hasPlugin('typescript')) {
/* eslint-disable-next-line node/no-extraneous-require */
const convertFiles = require('@vue/cli-plugin-typescript/generator/convert')

View File

@@ -0,0 +1,65 @@
---
extend: '@vue/cli-service/generator/template/src/App.vue'
replace:
- !!js/regexp /<template>[^]*?<\/template>/
- !!js/regexp /\n<script>[^]*?<\/script>\n/
- !!js/regexp / margin-top[^]*?<\/style>/
---
<%# REPLACE %>
<template>
<div id="app">
<div id="nav">
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</div>
<router-view/>
</div>
</template>
<%# END_REPLACE %>
<%# REPLACE %>
<%# END_REPLACE %>
<%# REPLACE %>
}
<%_ if (rootOptions.cssPreprocessor !== 'stylus') { _%>
<%_ if (!rootOptions.cssPreprocessor) { _%>
#nav {
padding: 30px;
}
#nav a {
font-weight: bold;
color: #2c3e50;
}
#nav a.router-link-exact-active {
color: #42b983;
}
<%_ } else { _%>
#nav {
padding: 30px;
a {
font-weight: bold;
color: #2c3e50;
&.router-link-exact-active {
color: #42b983;
}
}
}
<%_ } _%>
<%_ } else { _%>
#nav
padding 30px
a
font-weight bold
color #2c3e50
&.router-link-exact-active
color #42b983
<%_ } _%>
</style>
<%# END_REPLACE %>

View File

@@ -0,0 +1,37 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
<%_ if (doesCompile) { _%>
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
<%_ } else { _%>
component: function () {
return import(/* webpackChunkName: "about" */ '../views/About.vue')
}
<%_ } _%>
}
]
const router = new VueRouter({
<%_ if (historyMode) { _%>
mode: 'history',
base: process.env.BASE_URL,
<%_ } _%>
routes
})
export default router

View File

@@ -0,0 +1 @@
module.exports = (api, options = {}) => {}

View File

@@ -0,0 +1,31 @@
{
"name": "@vue/cli-plugin-router",
"version": "4.0.0-alpha.3",
"description": "router plugin for vue-cli",
"main": "index.js",
"repository": {
"type": "git",
"url": "git+https://github.com/vuejs/vue-cli.git",
"directory": "packages/@vue/cli-plugin-router"
},
"keywords": [
"vue",
"cli",
"router"
],
"author": "Evan You",
"license": "MIT",
"bugs": {
"url": "https://github.com/vuejs/vue-cli/issues"
},
"homepage": "https://github.com/vuejs/vue-cli/tree/dev/packages/@vue/cli-plugin-router#readme",
"publishConfig": {
"access": "public"
},
"dependencies": {
"@vue/cli-shared-utils": "^4.0.0-alpha.3"
},
"devDependencies": {
"@vue/cli-test-utils": "^4.0.0-alpha.3"
}
}

View File

@@ -0,0 +1,13 @@
// these prompts are used if the plugin is late-installed into an existing
// project and invoked by `vue invoke`.
const { chalk } = require('@vue/cli-shared-utils')
module.exports = [
{
name: 'historyMode',
type: 'confirm',
message: `Use history mode for router? ${chalk.yellow(`(Requires proper server setup for index fallback in production)`)}`,
description: `By using the HTML5 History API, the URLs don't need the '#' character anymore.`
}
]

View File

@@ -8,8 +8,8 @@ const create = require('@vue/cli-test-utils/createTestProject')
if (!process.env.APPVEYOR) {
test('cypress', async () => {
const project = await create('ts-e2e-cypress-router', {
router: true,
plugins: {
'@vue/cli-plugin-router': {},
'@vue/cli-plugin-typescript': {},
'@vue/cli-plugin-e2e-cypress': {}
}

View File

@@ -1,6 +1,6 @@
---
extend: '@vue/cli-service/generator/router/template/src/views/Home.vue'
when: 'rootOptions.router'
extend: '@vue/cli-plugin-router/generator/template/src/views/Home.vue'
when: "rootOptions.plugins && rootOptions.plugins['@vue/cli-plugin-router']"
replace:
- !!js/regexp /Welcome to Your Vue\.js App/
- !!js/regexp /<script>[^]*?<\/script>/

View File

@@ -29,7 +29,9 @@ test('serve', async () => {
test('serve with router', async () => {
const project = await create('e2e-serve-router', Object.assign({}, defaultPreset, {
router: true
plugins: {
'@vue/cli-plugin-router': {}
}
}))
await serve(
@@ -49,6 +51,29 @@ test('serve with router', async () => {
)
})
test('serve with legacy router option', async () => {
const project = await create('e2e-serve-legacy-router', Object.assign({}, defaultPreset, {
router: true,
routerHistoryMode: true
}))
await serve(
() => project.run('vue-cli-service serve'),
async ({ page, helpers }) => {
expect(await helpers.getText('h1')).toMatch(`Welcome to Your Vue.js App`)
expect(await helpers.hasElement('#nav')).toBe(true)
expect(await helpers.hasClass('a[href="/"]', 'router-link-exact-active')).toBe(true)
expect(await helpers.hasClass('a[href="/about"]', 'router-link-exact-active')).toBe(false)
await page.click('a[href="/about"]')
expect(await helpers.getText('h1')).toMatch(`This is an about page`)
expect(await helpers.hasElement('#nav')).toBe(true)
expect(await helpers.hasClass('a[href="/"]', 'router-link-exact-active')).toBe(false)
expect(await helpers.hasClass('a[href="/about"]', 'router-link-exact-active')).toBe(true)
}
)
})
test('serve with inline entry', async () => {
const project = await create('e2e-serve-inline-entry', defaultPreset)

View File

@@ -25,10 +25,6 @@ module.exports = (api, options) => {
]
})
if (options.router) {
require('./router')(api, options)
}
if (options.vuex) {
require('./vuex')(api, options)
}

View File

@@ -1,33 +0,0 @@
import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'
Vue.use(Router)
export default new Router({
<%_ if (historyMode) { _%>
mode: 'history',
base: process.env.BASE_URL,
<%_ } _%>
routes: [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
<%_ if (doesCompile) { _%>
component: () => import(/* webpackChunkName: "about" */ './views/About.vue')
<%_ } else { _%>
component: function () {
return import(/* webpackChunkName: "about" */ './views/About.vue')
}
<%_ } _%>
}
]
})

View File

@@ -1,4 +1,3 @@
<%_ if (!rootOptions.router) { _%>
<template>
<div id="app">
<img alt="Vue logo" src="./assets/logo.png">
@@ -9,7 +8,7 @@
<%_ } _%>
</div>
</template>
<%_ if (!rootOptions.bare) { _%>
<%_ if (!rootOptions.bare) { _%>
<script>
import HelloWorld from './components/HelloWorld.vue'
@@ -21,21 +20,8 @@ export default {
}
}
</script>
<%_ } _%>
<%_ } else { _%>
<template>
<div id="app">
<div id="nav">
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</div>
<router-view/>
</div>
</template>
<%_ } _%>
<%_ if (!rootOptions.bare) { _%>
<%_ if (rootOptions.cssPreprocessor !== 'stylus') { _%>
<%_ if (rootOptions.cssPreprocessor !== 'stylus') { _%>
<style<%-
rootOptions.cssPreprocessor
? ` lang="${
@@ -51,37 +37,8 @@ export default {
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
<%_ if (!rootOptions.router) { _%>
margin-top: 60px;
<%_ } _%>
}
<%_ if (rootOptions.router) { _%>
<%_ if (!rootOptions.cssPreprocessor) { _%>
#nav {
padding: 30px;
}
#nav a {
font-weight: bold;
color: #2c3e50;
}
#nav a.router-link-exact-active {
color: #42b983;
}
<%_ } else { _%>
#nav {
padding: 30px;
a {
font-weight: bold;
color: #2c3e50;
&.router-link-exact-active {
color: #42b983;
}
}
}
<%_ } _%>
<%_ } _%>
</style>
<%_ } else { _%>
<style lang="stylus">
@@ -91,19 +48,7 @@ export default {
-moz-osx-font-smoothing grayscale
text-align center
color #2c3e50
<%_ if (!rootOptions.router) { _%>
margin-top 60px
<%_ } _%>
<%_ if (rootOptions.router) { _%>
#nav
padding 30px
a
font-weight bold
color #2c3e50
&.router-link-exact-active
color #42b983
<%_ } _%>
</style>
<%_ } _%>
<%_ } _%>

View File

@@ -64,8 +64,7 @@ class PluginAPI {
* @return {boolean}
*/
hasPlugin (id) {
if (id === 'router') id = 'vue-router'
if (['vue-router', 'vuex'].includes(id)) {
if (['vuex'].includes(id)) {
const pkg = this.service.pkg
return ((pkg.dependencies && pkg.dependencies[id]) || (pkg.devDependencies && pkg.devDependencies[id]))
}

View File

@@ -397,8 +397,7 @@ class PluginApi {
* @param {string} id Plugin id or short id
*/
hasPlugin (id) {
if (id === 'router') id = 'vue-router'
if (['vue-router', 'vuex'].includes(id)) {
if (['vuex'].includes(id)) {
const pkg = folders.readPackage(this.cwd, this.context, true)
return ((pkg.dependencies && pkg.dependencies[id]) || (pkg.devDependencies && pkg.devDependencies[id]))
}

View File

@@ -1,7 +1,7 @@
describe('Plugins', () => {
it('Should display the plugins', () => {
cy.visit('/plugins')
cy.get('.project-plugin-item').should('have.length', 4)
cy.get('.project-plugin-item').should('have.length', 5)
})
it('Should add a plugin', () => {
@@ -26,6 +26,6 @@ describe('Plugins', () => {
.should('be.visible')
.should('not.have.class', 'disabled')
.click()
cy.get('.project-plugin-item').should('have.length', 4)
cy.get('.project-plugin-item').should('have.length', 5)
})
})

View File

@@ -8,7 +8,7 @@ const VUE_CONFIG_OPEN = 'org.vue.vue-config-open'
module.exports = api => {
api.onViewOpen(({ view }) => {
if (view.id === 'vue-project-plugins') {
if (!api.hasPlugin('vue-router')) {
if (!api.hasPlugin('router')) {
api.addSuggestion({
id: ROUTER,
type: 'action',
@@ -16,7 +16,7 @@ module.exports = api => {
message: 'org.vue.cli-service.suggestions.vue-router-add.message',
link: 'https://router.vuejs.org/',
async handler () {
await install(api, 'vue-router')
await install(api, 'router')
}
})
}
@@ -73,16 +73,19 @@ async function install (api, id) {
progress: -1
})
const name = id === 'vue-router' ? 'router' : id
const context = api.getCwd()
let error
try {
await invoke.runGenerator(context, {
id: `core:${name}`,
apply: loadModule(`@vue/cli-service/generator/${name}`, context)
})
if (id === 'router') {
await invoke(id, {}, context)
} else {
await invoke.runGenerator(context, {
id: `core:${id}`,
apply: loadModule(`@vue/cli-service/generator/${id}`, context)
})
}
} catch (e) {
error = e
}

View File

@@ -92,10 +92,20 @@ module.exports = class Creator extends EventEmitter {
preset.plugins['@vue/cli-service'] = Object.assign({
projectName: name
}, preset)
if (cliOptions.bare) {
preset.plugins['@vue/cli-service'].bare = true
}
// legacy support for router
if (preset.router) {
preset.plugins['@vue/cli-plugin-router'] = {}
if (preset.routerHistoryMode) {
preset.plugins['@vue/cli-plugin-router'].historyMode = true
}
}
const packageManager = (
cliOptions.packageManager ||
loadOptions().packageManager ||

View File

@@ -243,8 +243,7 @@ module.exports = class Generator {
}
hasPlugin (_id) {
if (_id === 'router') _id = 'vue-router'
if (['vue-router', 'vuex'].includes(_id)) {
if (['vuex'].includes(_id)) {
const pkg = this.pkg
return ((pkg.dependencies && pkg.dependencies[_id]) || (pkg.devDependencies && pkg.devDependencies[_id]))
}

View File

@@ -14,9 +14,6 @@ const {
async function add (pluginName, options = {}, context = process.cwd()) {
// special internal "plugins"
if (/^(@vue\/)?router$/.test(pluginName)) {
return addRouter(context)
}
if (/^(@vue\/)?vuex$/.test(pluginName)) {
return addVuex(context)
}
@@ -41,20 +38,6 @@ async function add (pluginName, options = {}, context = process.cwd()) {
}
}
async function addRouter (context) {
const inquirer = require('inquirer')
const options = await inquirer.prompt([{
name: 'routerHistoryMode',
type: 'confirm',
message: `Use history mode for router? ${chalk.yellow(`(Requires proper server setup for index fallback in production)`)}`
}])
invoke.runGenerator(context, {
id: 'core:router',
apply: loadModule('@vue/cli-service/generator/router', context),
options
})
}
async function addVuex (context) {
invoke.runGenerator(context, {
id: 'core:vuex',

View File

@@ -10,6 +10,7 @@ const rcPath = exports.rcPath = getRcPath('.vuerc')
const presetSchema = createSchema(joi => joi.object().keys({
bare: joi.boolean(),
useConfigFiles: joi.boolean(),
// TODO: Use warn for router once @hapi/joi v16 releases
router: joi.boolean(),
routerHistoryMode: joi.boolean(),
vuex: joi.boolean(),
@@ -31,7 +32,6 @@ exports.validatePreset = preset => validate(preset, presetSchema, msg => {
})
exports.defaultPreset = {
router: false,
vuex: false,
useConfigFiles: false,
cssPreprocessor: undefined,

View File

@@ -19,9 +19,11 @@ test('router', async () => {
]
const expectedOptions = {
router: true,
routerHistoryMode: true,
plugins: {}
plugins: {
'@vue/cli-plugin-router': {
historyMode: true
}
}
}
await assertPromptModule(

View File

@@ -9,7 +9,7 @@ module.exports = cli => {
})
cli.injectPrompt({
name: 'routerHistoryMode',
name: 'historyMode',
when: answers => answers.features.includes('router'),
type: 'confirm',
message: `Use history mode for router? ${chalk.yellow(`(Requires proper server setup for index fallback in production)`)}`,
@@ -19,8 +19,9 @@ module.exports = cli => {
cli.onPromptComplete((answers, options) => {
if (answers.features.includes('router')) {
options.router = true
options.routerHistoryMode = answers.routerHistoryMode
options.plugins['@vue/cli-plugin-router'] = {
historyMode: answers.historyMode
}
}
})
}

View File

@@ -4,7 +4,7 @@ const { toShortPluginId } = require('@vue/cli-shared-utils')
exports.getFeatures = (preset) => {
const features = []
if (preset.router) {
features.push('vue-router')
features.push('router')
}
if (preset.vuex) {
features.push('vuex')