diff --git a/packages/@vue/cli-plugin-babel/migrator/index.js b/packages/@vue/cli-plugin-babel/migrator/index.js index 9263c767ed..daae8778fc 100644 --- a/packages/@vue/cli-plugin-babel/migrator/index.js +++ b/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. diff --git a/packages/@vue/cli-plugin-eslint/migrator/index.js b/packages/@vue/cli-plugin-eslint/migrator/index.js index aa00200496..08b03584e9 100644 --- a/packages/@vue/cli-plugin-eslint/migrator/index.js +++ b/packages/@vue/cli-plugin-eslint/migrator/index.js @@ -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')) { diff --git a/packages/@vue/cli-plugin-typescript/migrator/index.js b/packages/@vue/cli-plugin-typescript/migrator/index.js index 6320111115..a28ea4d578 100644 --- a/packages/@vue/cli-plugin-typescript/migrator/index.js +++ b/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 } + ) } diff --git a/packages/@vue/cli/__tests__/Generator.spec.js b/packages/@vue/cli/__tests__/Generator.spec.js index 7bbd4607aa..77a780cb95 100644 --- a/packages/@vue/cli/__tests__/Generator.spec.js +++ b/packages/@vue/cli/__tests__/Generator.spec.js @@ -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: { @@ -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: [ { diff --git a/packages/@vue/cli/lib/GeneratorAPI.js b/packages/@vue/cli/lib/GeneratorAPI.js index 640f220858..8b0906e7dd 100644 --- a/packages/@vue/cli/lib/GeneratorAPI.js +++ b/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') @@ -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 { /** @@ -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) { @@ -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) + } } /** diff --git a/packages/@vue/cli/lib/util/mergeDeps.js b/packages/@vue/cli/lib/util/mergeDeps.js index b7b0bf5798..410f02f487 100644 --- a/packages/@vue/cli/lib/util/mergeDeps.js +++ b/packages/@vue/cli/lib/util/mergeDeps.js @@ -3,61 +3,93 @@ const { semver, warn } = require('@vue/cli-shared-utils') const tryGetNewerRange = require('./tryGetNewerRange') const extractSemver = r => r.replace(/^.+#semver:/, '') -const injectSemver = (r, v) => semver.validRange(r) ? v : r.replace(/#semver:.+$/, `#semver:${v}`) - -module.exports = function resolveDeps (generatorId, to, from, sources, forceNewVersion) { - const res = Object.assign({}, to) - for (const name in from) { - const r1 = to[name] - const r2 = from[name] - const r2IsString = typeof r2 === 'string' - const sourceGeneratorId = sources[name] - const isValidURI = r2IsString && r2.match(/^(?:file|git|git\+ssh|git\+http|git\+https|git\+file|https?):/) != null - const isValidGitHub = r2IsString && r2.match(/^[^/]+\/[^/]+/) != null +const injectSemver = (r, v) => + semver.validRange(r) ? v : r.replace(/#semver:.+$/, `#semver:${v}`) + +const isValidRange = range => { + if (typeof range !== 'string') { + return false + } + + const isValidSemver = !!semver.validRange(range) + const isValidGitHub = range.match(/^[^/]+\/[^/]+/) != null + const isValidURI = + range.match( + /^(?:file|git|git\+ssh|git\+http|git\+https|git\+file|https?):/ + ) != null + + return isValidSemver || isValidGitHub || isValidURI +} + +module.exports = function mergeDeps ( + generatorId, + sourceDeps, + depsToInject, + sources, + { + prune, + warnIncompatibleVersions + } +) { + const result = Object.assign({}, sourceDeps) + + for (const depName in depsToInject) { + const sourceRange = sourceDeps[depName] + const injectingRange = depsToInject[depName] // if they are the same, do nothing. Helps when non semver type deps are used - if (r1 === r2) continue + if (sourceRange === injectingRange) continue + + if (prune && injectingRange == null) { + delete result[depName] + } - if (!isValidGitHub && !isValidURI && !semver.validRange(r2)) { + if (!isValidRange(injectingRange)) { warn( - `invalid version range for dependency "${name}":\n\n` + - `- ${r2} injected by generator "${generatorId}"` + `invalid version range for dependency "${depName}":\n\n` + + `- ${injectingRange} injected by generator "${generatorId}"` ) continue } - if (!r1) { - res[name] = r2 - sources[name] = generatorId + const sourceGeneratorId = sources[depName] + if (!sourceRange) { + result[depName] = injectingRange + sources[depName] = generatorId } else { - const r1semver = extractSemver(r1) - const r2semver = extractSemver(r2) - const r = tryGetNewerRange(r1semver, r2semver) + const sourceRangeSemver = extractSemver(sourceRange) + const injectingRangeSemver = extractSemver(injectingRange) + const r = tryGetNewerRange(sourceRangeSemver, injectingRangeSemver) const didGetNewer = !!r + // if failed to infer newer version, use existing one because it's likely // built-in - res[name] = didGetNewer ? injectSemver(r2, r) : r1 + result[depName] = didGetNewer + ? injectSemver(injectingRange, r) + : sourceRange + // if changed, update source - if (res[name] === r2) { - sources[name] = generatorId + if (result[depName] === injectingRange) { + sources[depName] = generatorId } + // warn incompatible version requirements if ( - !forceNewVersion && - ( - !semver.validRange(r1semver) || - !semver.validRange(r2semver) || - !semver.intersects(r1semver, r2semver) - ) + warnIncompatibleVersions && + (!semver.validRange(sourceRangeSemver) || + !semver.validRange(injectingRangeSemver) || + !semver.intersects(sourceRangeSemver, injectingRangeSemver)) ) { warn( - `conflicting versions for project dependency "${name}":\n\n` + - `- ${r1} injected by generator "${sourceGeneratorId}"\n` + - `- ${r2} injected by generator "${generatorId}"\n\n` + - `Using ${didGetNewer ? `newer ` : ``}version (${res[name]}), but this may cause build errors.` + `conflicting versions for project dependency "${depName}":\n\n` + + `- ${sourceRange} injected by generator "${sourceGeneratorId}"\n` + + `- ${injectingRange} injected by generator "${generatorId}"\n\n` + + `Using ${didGetNewer ? `newer ` : ``}version (${ + result[depName] + }), but this may cause build errors.` ) } } } - return res + return result }