Skip to content

Commit

Permalink
feat(GeneratorAPI): allow passing options to api.extendPackage (vue…
Browse files Browse the repository at this point in the history
…js#5149)

Currently, 3 options are implemented:

- options.prune (defaults to `false`) - Remove null or undefined
fields from the object after merging.
- options.merge (defaults to `true`) deep-merge nested fields, note
that dependency fields are always deep merged regardless of this option.
- options.warnIncompatibleVersions (defaults to `true`) Output warning
if two dependency version ranges don't intersect.

Closes vuejs#4779
  • Loading branch information
sodatea authored and mactanxin committed Feb 11, 2020
1 parent a35f8ea commit 4aa8aff
Show file tree
Hide file tree
Showing 6 changed files with 265 additions and 56 deletions.
20 changes: 13 additions & 7 deletions packages/@vue/cli-plugin-babel/migrator/index.js
@@ -1,14 +1,20 @@
const { chalk } = require('@vue/cli-shared-utils')

module.exports = (api) => {
api.transformScript('babel.config.js', require('../codemods/usePluginPreset'))
module.exports = api => {
api.transformScript(
'babel.config.js',
require('../codemods/usePluginPreset')
)

if (api.fromVersion('^3')) {
api.extendPackage({
dependencies: {
'core-js': '^3.6.4'
}
}, true)
api.extendPackage(
{
dependencies: {
'core-js': '^3.6.4'
}
},
{ warnIncompatibleVersions: false }
)

// TODO: implement a codemod to migrate polyfills
api.exitLog(`core-js has been upgraded from v2 to v3.
Expand Down
2 changes: 1 addition & 1 deletion packages/@vue/cli-plugin-eslint/migrator/index.js
Expand Up @@ -54,7 +54,7 @@ module.exports = async (api) => {
Object.assign(newDeps, getDeps(api, 'prettier'))
}

api.extendPackage({ devDependencies: newDeps }, true)
api.extendPackage({ devDependencies: newDeps }, { warnIncompatibleVersions: false })

// in case anyone's upgrading from the legacy `typescript-eslint-parser`
if (api.hasPlugin('typescript')) {
Expand Down
15 changes: 9 additions & 6 deletions packages/@vue/cli-plugin-typescript/migrator/index.js
@@ -1,7 +1,10 @@
module.exports = (api) => {
api.extendPackage({
devDependencies: {
typescript: require('../package.json').devDependencies.typescript
}
}, true)
module.exports = api => {
api.extendPackage(
{
devDependencies: {
typescript: require('../package.json').devDependencies.typescript
}
},
{ warnIncompatibleVersions: false }
)
}
128 changes: 128 additions & 0 deletions packages/@vue/cli/__tests__/Generator.spec.js
Expand Up @@ -73,6 +73,10 @@ fs.ensureDirSync(path.resolve(templateDir, '_vscode'))
fs.writeFileSync(path.resolve(templateDir, '_vscode/config.json'), `{}`)
fs.writeFileSync(path.resolve(templateDir, '_gitignore'), 'foo')

beforeEach(() => {
logs.warn = []
})

test('api: extendPackage', async () => {
const generator = new Generator('/', {
pkg: {
Expand Down Expand Up @@ -376,6 +380,130 @@ test('api: extendPackage merge warn nonstrictly semver deps', async () => {
})).toBe(true)
})

test('api: extendPackage + { merge: false }', async () => {
const generator = new Generator('/', {
pkg: {
name: 'hello',
list: [1],
vue: {
foo: 1,
bar: 2
}
},
plugins: [{
id: 'test',
apply: api => {
api.extendPackage(
{
name: 'hello2',
list: [2],
vue: {
foo: 2,
baz: 3
}
},
{ merge: false }
)
}
}]
})

await generator.generate()

const pkg = JSON.parse(fs.readFileSync('/package.json', 'utf-8'))
expect(pkg).toEqual({
name: 'hello2',
list: [2],
vue: {
foo: 2,
baz: 3
}
})
})

test('api: extendPackage + { prune: true }', async () => {
const generator = new Generator('/', {
pkg: {
name: 'hello',
version: '0.0.0',
dependencies: {
foo: '1.0.0'
},
vue: {
bar: 1,
baz: 2
}
},
plugins: [{
id: 'test',
apply: api => {
api.extendPackage(
{
name: null,
dependencies: {
foo: null,
qux: '2.0.0'
},
vue: {
bar: null,
baz: 3
}
},
{ prune: true }
)
}
}]
})

await generator.generate()

const pkg = JSON.parse(fs.readFileSync('/package.json', 'utf-8'))
expect(pkg).toEqual({
version: '0.0.0',
dependencies: {
qux: '2.0.0'
},
vue: {
baz: 3
}
})
})

test('api: extendPackage + { warnIncompatibleVersions: false }', async () => {
const generator = new Generator('/', {
pkg: {
devDependencies: {
eslint: '^4.0.0'
}
},
plugins: [{
id: 'test',
apply: api => {
api.extendPackage(
{
devDependencies: {
eslint: '^6.0.0'
}
},
{ warnIncompatibleVersions: false }
)
}
}]
})

await generator.generate()
const pkg = JSON.parse(fs.readFileSync('/package.json', 'utf-8'))

// should not warn about the version conflicts
expect(logs.warn.length).toBe(0)
// should use the newer version
expect(pkg).toEqual({
devDependencies: {
eslint: '^6.0.0'
}
})
})

test('api: render fs directory', async () => {
const generator = new Generator('/', { plugins: [
{
Expand Down
54 changes: 47 additions & 7 deletions packages/@vue/cli/lib/GeneratorAPI.js
@@ -1,7 +1,7 @@
const fs = require('fs')
const ejs = require('ejs')
const path = require('path')
const merge = require('deepmerge')
const deepmerge = require('deepmerge')
const resolve = require('resolve')
const { isBinaryFileSync } = require('isbinaryfile')
const mergeDeps = require('./util/mergeDeps')
Expand All @@ -14,6 +14,23 @@ const isString = val => typeof val === 'string'
const isFunction = val => typeof val === 'function'
const isObject = val => val && typeof val === 'object'
const mergeArrayWithDedupe = (a, b) => Array.from(new Set([...a, ...b]))
function pruneObject (obj) {
if (typeof obj === 'object') {
for (const k in obj) {
if (!obj.hasOwnProperty(k)) {
continue
}

if (obj[k] == null) {
delete obj[k]
} else {
obj[k] = pruneObject(obj[k])
}
}
}

return obj
}

class GeneratorAPI {
/**
Expand Down Expand Up @@ -176,15 +193,34 @@ class GeneratorAPI {

/**
* Extend the package.json of the project.
* Nested fields are deep-merged unless `{ merge: false }` is passed.
* Also resolves dependency conflicts between plugins.
* Tool configuration fields may be extracted into standalone files before
* files are written to disk.
*
* @param {object | () => object} fields - Fields to merge.
* @param {boolean} forceNewVersion - Ignore version conflicts when updating dependency version
* @param {object} [options] - Options for extending / merging fields.
* @param {boolean} [options.prune=false] - Remove null or undefined fields
* from the object after merging.
* @param {boolean} [options.merge=true] deep-merge nested fields, note
* that dependency fields are always deep merged regardless of this option.
* @param {boolean} [options.warnIncompatibleVersions=true] Output warning
* if two dependency version ranges don't intersect.
*/
extendPackage (fields, forceNewVersion) {
extendPackage (fields, options = {}) {
const extendOptions = {
prune: false,
merge: true,
warnIncompatibleVersions: true
}

// this condition statement is added for compatiblity reason, because
// in version 4.0.0 to 4.1.2, there's no `options` object, but a `forceNewVersion` flag
if (typeof options === 'boolean') {
extendOptions.warnIncompatibleVersions = !options
} else {
Object.assign(extendOptions, options)
}

const pkg = this.generator.pkg
const toMerge = isFunction(fields) ? fields(pkg) : fields
for (const key in toMerge) {
Expand All @@ -197,18 +233,22 @@ class GeneratorAPI {
existing || {},
value,
this.generator.depSources,
forceNewVersion
extendOptions
)
} else if (!(key in pkg)) {
} else if (!extendOptions.merge || !(key in pkg)) {
pkg[key] = value
} else if (Array.isArray(value) && Array.isArray(existing)) {
pkg[key] = mergeArrayWithDedupe(existing, value)
} else if (isObject(value) && isObject(existing)) {
pkg[key] = merge(existing, value, { arrayMerge: mergeArrayWithDedupe })
pkg[key] = deepmerge(existing, value, { arrayMerge: mergeArrayWithDedupe })
} else {
pkg[key] = value
}
}

if (extendOptions.prune) {
pruneObject(pkg)
}
}

/**
Expand Down

0 comments on commit 4aa8aff

Please sign in to comment.