Skip to content

Commit 46166fb

Browse files
committed
fix: handle vue invoke config merging for existing files
close #788
1 parent 1902d9c commit 46166fb

14 files changed

+386
-107
lines changed

packages/@vue/cli-test-utils/createTestProject.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const execa = require('execa')
44
const { promisify } = require('util')
55
const readFile = promisify(fs.readFile)
66
const writeFile = promisify(fs.writeFile)
7+
const rmFile = promisify(fs.unlink)
78
const mkdirp = promisify(require('mkdirp'))
89

910
module.exports = function createTestProject (name, preset, cwd) {
@@ -25,6 +26,10 @@ module.exports = function createTestProject (name, preset, cwd) {
2526
return mkdirp(dir).then(() => writeFile(targetPath, content))
2627
}
2728

29+
const rm = file => {
30+
return rmFile(path.resolve(projectRoot, file))
31+
}
32+
2833
const run = (command, args) => {
2934
[command, ...args] = command.split(/\s+/)
3035
if (command === 'vue-cli-service') {
@@ -54,6 +59,7 @@ module.exports = function createTestProject (name, preset, cwd) {
5459
has,
5560
read,
5661
write,
57-
run
62+
run,
63+
rm
5864
}))
5965
}

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

+5-38
Original file line numberDiff line numberDiff line change
@@ -67,41 +67,6 @@ test('api: extendPackage function', async () => {
6767
})
6868
})
6969

70-
test('api: extendPackage + { merge: false }', async () => {
71-
const generator = new Generator('/', {
72-
name: 'hello',
73-
list: [1],
74-
vue: {
75-
foo: 1,
76-
bar: 2
77-
}
78-
}, [{
79-
id: 'test',
80-
apply: api => {
81-
api.extendPackage({
82-
name: 'hello2',
83-
list: [2],
84-
vue: {
85-
foo: 2,
86-
baz: 3
87-
}
88-
}, { merge: false })
89-
}
90-
}])
91-
92-
await generator.generate()
93-
94-
const pkg = JSON.parse(fs.readFileSync('/package.json', 'utf-8'))
95-
expect(pkg).toEqual({
96-
name: 'hello2',
97-
list: [2],
98-
vue: {
99-
foo: 2,
100-
baz: 3
101-
}
102-
})
103-
})
104-
10570
test('api: extendPackage merge dependencies', async () => {
10671
const generator = new Generator('/', {}, [
10772
{
@@ -292,7 +257,7 @@ test('api: onCreateComplete', () => {
292257
api.onCreateComplete(fn)
293258
}
294259
}
295-
], false, cbs)
260+
], cbs)
296261
expect(cbs).toContain(fn)
297262
})
298263

@@ -333,9 +298,11 @@ test('extract config files', async () => {
333298
api.extendPackage(configs)
334299
}
335300
}
336-
], true)
301+
])
337302

338-
await generator.generate()
303+
await generator.generate({
304+
extractConfigFiles: true
305+
})
339306

340307
const json = v => JSON.stringify(v, null, 2)
341308
expect(fs.readFileSync('/vue.config.js', 'utf-8')).toMatch('module.exports = {\n lintOnSave: true\n}')

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

+69-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
jest.setTimeout(10000)
1+
jest.setTimeout(12000)
22
jest.mock('inquirer')
33

44
const invoke = require('../lib/invoke')
@@ -22,13 +22,15 @@ async function assertUpdates (project) {
2222
const updatedPkg = JSON.parse(await project.read('package.json'))
2323
expect(updatedPkg.scripts.lint).toBe('vue-cli-service lint')
2424
expect(updatedPkg.devDependencies).toHaveProperty('lint-staged')
25-
expect(updatedPkg.eslintConfig).toEqual({
26-
extends: ['plugin:vue/essential', '@vue/airbnb']
27-
})
2825
expect(updatedPkg.gitHooks).toEqual({
2926
'pre-commit': 'lint-staged'
3027
})
3128

29+
const eslintrc = JSON.parse(await project.read('.eslintrc'))
30+
expect(eslintrc).toEqual({
31+
extends: ['plugin:vue/essential', '@vue/airbnb']
32+
})
33+
3234
const lintedMain = await project.read('src/main.js')
3335
expect(lintedMain).toMatch(';') // should've been linted in post-generate hook
3436
}
@@ -58,3 +60,66 @@ test('invoke with prompts', async () => {
5860
await invoke(`eslint`, {}, project.dir)
5961
await assertUpdates(project)
6062
})
63+
64+
test('invoke with existing files', async () => {
65+
const project = await create(`invoke-existing`, {
66+
useConfigFiles: true,
67+
plugins: {
68+
'@vue/cli-plugin-babel': {},
69+
'@vue/cli-plugin-eslint': { config: 'base' }
70+
}
71+
})
72+
// mock install
73+
const pkg = JSON.parse(await project.read('package.json'))
74+
pkg.devDependencies['@vue/cli-plugin-eslint'] = '*'
75+
await project.write('package.json', JSON.stringify(pkg, null, 2))
76+
77+
// mock existing vue.config.js
78+
await project.write('vue.config.js', `module.exports = { lintOnSave: false }`)
79+
80+
const eslintrc = JSON.parse(await project.read('.eslintrc'))
81+
expect(eslintrc).toEqual({
82+
extends: ['plugin:vue/essential', 'eslint:recommended']
83+
})
84+
85+
await project.run(`${require.resolve('../bin/vue')} invoke eslint --config airbnb --lintOn save,commit`)
86+
87+
await assertUpdates(project)
88+
const updatedVueConfig = await project.read('vue.config.js')
89+
expect(updatedVueConfig).toMatch(`module.exports = { lintOnSave: true }`)
90+
})
91+
92+
test('invoke with existing files (yaml)', async () => {
93+
const project = await create(`invoke-existing`, {
94+
useConfigFiles: true,
95+
plugins: {
96+
'@vue/cli-plugin-babel': {},
97+
'@vue/cli-plugin-eslint': { config: 'base' }
98+
}
99+
})
100+
// mock install
101+
const pkg = JSON.parse(await project.read('package.json'))
102+
pkg.devDependencies['@vue/cli-plugin-eslint'] = '*'
103+
await project.write('package.json', JSON.stringify(pkg, null, 2))
104+
105+
const eslintrc = JSON.parse(await project.read('.eslintrc'))
106+
expect(eslintrc).toEqual({
107+
extends: ['plugin:vue/essential', 'eslint:recommended']
108+
})
109+
110+
await project.rm(`.eslintrc`)
111+
await project.write(`.eslintrc.yml`, `
112+
extends:
113+
- 'plugin:vue/essential'
114+
- 'eslint:recommended'
115+
`.trim())
116+
117+
await project.run(`${require.resolve('../bin/vue')} invoke eslint --config airbnb`)
118+
119+
const updated = await project.read('.eslintrc.yml')
120+
expect(updated).toMatch(`
121+
extends:
122+
- 'plugin:vue/essential'
123+
- '@vue/airbnb'
124+
`.trim())
125+
})

packages/@vue/cli/acorn-test.js

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
const acorn = require('acorn')
2+
const walk = require('acorn/dist/walk')
3+
4+
const ast = acorn.parse(`
5+
module.exports = {
6+
lintOnSave: true,
7+
css: {
8+
loaderOptions: {
9+
sass: {
10+
data: 'foo'
11+
}
12+
}
13+
},
14+
pluginsOptions: {
15+
foo: 'bar'
16+
}
17+
}
18+
`)
19+
20+
let exportsIdentifier = null
21+
22+
walk.simple(ast, {
23+
AssignmentExpression (node) {
24+
if (
25+
node.left.type === 'MemberExpression' &&
26+
node.left.object.name === 'module' &&
27+
node.left.property.name === 'exports'
28+
) {
29+
if (node.right.type === 'ObjectExpression') {
30+
augmentExports(node.right)
31+
} else if (node.right.type === 'Identifier') {
32+
// do a second pass
33+
exportsIdentifier = node.right.name
34+
}
35+
}
36+
}
37+
})
38+
39+
if (exportsIdentifier) {
40+
walk.simple(ast, {
41+
VariableDeclarator (node) {
42+
if (
43+
node.id.name === exportsIdentifier &&
44+
node.init.type === 'ObjectExpression'
45+
) {
46+
augmentExports(node.init)
47+
}
48+
}
49+
})
50+
}
51+
52+
function augmentExports (node) {
53+
console.log(node)
54+
}

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

+3-2
Original file line numberDiff line numberDiff line change
@@ -136,10 +136,11 @@ module.exports = class Creator {
136136
context,
137137
pkg,
138138
plugins,
139-
preset.useConfigFiles,
140139
createCompleteCbs
141140
)
142-
await generator.generate()
141+
await generator.generate({
142+
extractConfigFiles: preset.useConfigFiles
143+
})
143144

144145
// install additional deps (injected by generators)
145146
log(`📦 Installing additional dependencies...`)

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

+27-12
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
const ejs = require('ejs')
22
const slash = require('slash')
33
const debug = require('debug')
4-
const configMap = require('./util/configMap')
54
const GeneratorAPI = require('./GeneratorAPI')
65
const sortObject = require('./util/sortObject')
76
const writeFileTree = require('./util/writeFileTree')
7+
const configTransforms = require('./util/configTransforms')
88

99
module.exports = class Generator {
10-
constructor (context, pkg, plugins, extractConfigFiles, completeCbs = []) {
10+
constructor (context, pkg, plugins, completeCbs = []) {
1111
this.context = context
1212
this.plugins = plugins
13-
this.pkg = pkg
13+
this.originalPkg = pkg
14+
this.pkg = Object.assign({}, pkg)
1415
this.completeCbs = completeCbs
1516

1617
// for conflict resolution
@@ -27,11 +28,14 @@ module.exports = class Generator {
2728
const api = new GeneratorAPI(id, this, options, rootOptions || {})
2829
apply(api, options, rootOptions)
2930
})
30-
// extract configs from package.json into dedicated files.
31-
this.extractConfigFiles(extractConfigFiles)
3231
}
3332

34-
async generate () {
33+
async generate ({
34+
extractConfigFiles = false,
35+
checkExisting = false
36+
} = {}) {
37+
// extract configs from package.json into dedicated files.
38+
this.extractConfigFiles(extractConfigFiles, checkExisting)
3539
// wait for file resolve
3640
await this.resolveFiles()
3741
// set package.json
@@ -41,21 +45,32 @@ module.exports = class Generator {
4145
await writeFileTree(this.context, this.files)
4246
}
4347

44-
extractConfigFiles (all) {
48+
extractConfigFiles (extractAll, checkExisting) {
4549
const extract = key => {
46-
if (configMap[key]) {
50+
if (
51+
configTransforms[key] &&
52+
this.pkg[key] &&
53+
// do not extract if the field exists in original package.json
54+
!this.originalPkg[key]
55+
) {
4756
const value = this.pkg[key]
48-
const { transform, filename } = configMap[key]
49-
this.files[filename] = transform(value)
57+
const transform = configTransforms[key]
58+
const res = transform(
59+
value,
60+
checkExisting,
61+
this.context
62+
)
63+
const { content, filename } = res
64+
this.files[filename] = content
5065
delete this.pkg[key]
5166
}
5267
}
53-
if (all) {
68+
if (extractAll) {
5469
for (const key in this.pkg) {
5570
extract(key)
5671
}
5772
} else if (!process.env.VUE_CLI_TEST) {
58-
// by default, extract vue.config.js
73+
// by default, always extract vue.config.js
5974
extract('vue')
6075
}
6176
}

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

+3-4
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,9 @@ class GeneratorAPI {
100100
* Tool configuration fields may be extracted into standalone files before
101101
* files are written to disk.
102102
*
103-
* @param {object} fields - Fields to merge.
104-
* @param {object} [options] - pass { merge: false } to disable deep merging.
103+
* @param {object | () => object} fields - Fields to merge.
105104
*/
106-
extendPackage (fields, options = { merge: true }) {
105+
extendPackage (fields) {
107106
const pkg = this.generator.pkg
108107
const toMerge = isFunction(fields) ? fields(pkg) : fields
109108
for (const key in toMerge) {
@@ -117,7 +116,7 @@ class GeneratorAPI {
117116
value,
118117
this.generator.depSources
119118
)
120-
} else if (!options.merge || !(key in pkg)) {
119+
} else if (!(key in pkg)) {
121120
pkg[key] = value
122121
} else if (Array.isArray(value) && Array.isArray(existing)) {
123122
pkg[key] = existing.concat(value)

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

+9-5
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,15 @@ async function invoke (pluginName, options = {}, context = process.cwd()) {
8181
context,
8282
pkg,
8383
[plugin],
84-
isTestOrDebug ? false : loadOptions().useConfigFiles,
8584
createCompleteCbs
8685
)
8786

8887
log()
8988
logWithSpinner('🚀', `Invoking generator for ${id}...`)
90-
await generator.generate()
89+
await generator.generate({
90+
extractConfigFiles: true,
91+
checkExisting: true
92+
})
9193

9294
const newDeps = generator.pkg.dependencies
9395
const newDevDeps = generator.pkg.devDependencies
@@ -115,9 +117,11 @@ async function invoke (pluginName, options = {}, context = process.cwd()) {
115117
log(` Successfully invoked generator for plugin: ${chalk.cyan(id)}`)
116118
if (!process.env.VUE_CLI_TEST && hasGit()) {
117119
const { stdout } = await execa('git', ['ls-files', '--exclude-standard', '--modified', '--others'])
118-
log(` The following files have been updated / added:\n`)
119-
log(chalk.red(stdout.split(/\r?\n/g).map(line => ` ${line}`).join('\n')))
120-
log()
120+
if (stdout.trim()) {
121+
log(` The following files have been updated / added:\n`)
122+
log(chalk.red(stdout.split(/\r?\n/g).map(line => ` ${line}`).join('\n')))
123+
log()
124+
}
121125
}
122126
log(` You should review and commit the changes.`)
123127
log()

0 commit comments

Comments
 (0)