diff --git a/packages/@vue/cli-plugin-eslint/ui.js b/packages/@vue/cli-plugin-eslint/ui.js index eb97da5e6d..a0a115baa3 100644 --- a/packages/@vue/cli-plugin-eslint/ui.js +++ b/packages/@vue/cli-plugin-eslint/ui.js @@ -11,6 +11,7 @@ module.exports = api => { eslint: { js: ['.eslintrc.js'], json: ['.eslintrc', '.eslintrc.json'], + yaml: ['.eslintrc.yaml', '.eslintrc.yml'], package: 'eslintConfig' }, vue: { diff --git a/packages/@vue/cli-plugin-pwa/lib/HtmlPwaPlugin.js b/packages/@vue/cli-plugin-pwa/lib/HtmlPwaPlugin.js index 23538ca802..b5f060054d 100644 --- a/packages/@vue/cli-plugin-pwa/lib/HtmlPwaPlugin.js +++ b/packages/@vue/cli-plugin-pwa/lib/HtmlPwaPlugin.js @@ -22,7 +22,7 @@ module.exports = class HtmlPwaPlugin { constructor (options = {}) { const iconPaths = Object.assign({}, defaultIconPaths, options.iconPaths) delete options.iconPaths - this.options = Object.assign({iconPaths: iconPaths}, defaults, options) + this.options = Object.assign({ iconPaths: iconPaths }, defaults, options) } apply (compiler) { diff --git a/packages/@vue/cli/__tests__/Generator.spec.js b/packages/@vue/cli/__tests__/Generator.spec.js index c14857ea2b..3274ea974e 100644 --- a/packages/@vue/cli/__tests__/Generator.spec.js +++ b/packages/@vue/cli/__tests__/Generator.spec.js @@ -510,6 +510,98 @@ test('api: addEntryDuplicateNonIdentifierInjection', async () => { expect(fs.readFileSync('/main.js', 'utf-8')).toMatch(/{\s+p: p\(\),\s+baz,\s+render/) }) +test('api: addConfigTransform', async () => { + const configs = { + fooConfig: { + bar: 42 + } + } + + const generator = new Generator('/', { plugins: [ + { + id: 'test', + apply: api => { + api.addConfigTransform('fooConfig', { + file: { + json: ['foo.config.json'] + } + }) + api.extendPackage(configs) + } + } + ] }) + + await generator.generate({ + extractConfigFiles: true + }) + + const json = v => JSON.stringify(v, null, 2) + expect(fs.readFileSync('/foo.config.json', 'utf-8')).toMatch(json(configs.fooConfig)) + expect(generator.pkg).not.toHaveProperty('fooConfig') +}) + +test('api: addConfigTransform (multiple)', async () => { + const configs = { + bazConfig: { + field: 2501 + } + } + + const generator = new Generator('/', { plugins: [ + { + id: 'test', + apply: api => { + api.addConfigTransform('bazConfig', { + file: { + js: ['.bazrc.js'], + json: ['.bazrc', 'baz.config.json'] + } + }) + api.extendPackage(configs) + } + } + ] }) + + await generator.generate({ + extractConfigFiles: true + }) + + const js = v => `module.exports = ${stringifyJS(v, null, 2)}` + expect(fs.readFileSync('/.bazrc.js', 'utf-8')).toMatch(js(configs.bazConfig)) + expect(generator.pkg).not.toHaveProperty('bazConfig') +}) + +test('api: addConfigTransform transform vue warn', async () => { + const configs = { + vue: { + lintOnSave: true + } + } + + const generator = new Generator('/', { plugins: [ + { + id: 'test', + apply: api => { + api.addConfigTransform('vue', { + file: { + js: ['vue.config.js'] + } + }) + api.extendPackage(configs) + } + } + ] }) + + await generator.generate({ + extractConfigFiles: true + }) + + expect(fs.readFileSync('/vue.config.js', 'utf-8')).toMatch('module.exports = {\n lintOnSave: true\n}') + expect(logs.warn.some(([msg]) => { + return msg.match(/Reserved config transform 'vue'/) + })).toBe(true) +}) + test('extract config files', async () => { const configs = { vue: { diff --git a/packages/@vue/cli/lib/ConfigTransform.js b/packages/@vue/cli/lib/ConfigTransform.js new file mode 100644 index 0000000000..6aa90325a9 --- /dev/null +++ b/packages/@vue/cli/lib/ConfigTransform.js @@ -0,0 +1,65 @@ +const transforms = require('./util/configTransforms') + +class ConfigTransform { + constructor (options) { + this.fileDescriptor = options.file + } + + transform (value, checkExisting, files, context) { + let file + if (checkExisting) { + file = this.findFile(files) + } + if (!file) { + file = this.getDefaultFile() + } + const { type, filename } = file + + const transform = transforms[type] + + let source + let existing + if (checkExisting) { + source = files[filename] + if (source) { + existing = transform.read({ + source, + filename, + context + }) + } + } + + const content = transform.write({ + source, + filename, + context, + value, + existing + }) + + return { + filename, + content + } + } + + findFile (files) { + for (const type of Object.keys(this.fileDescriptor)) { + const descriptors = this.fileDescriptor[type] + for (const filename of descriptors) { + if (files[filename]) { + return { type, filename } + } + } + } + } + + getDefaultFile () { + const [type] = Object.keys(this.fileDescriptor) + const [filename] = this.fileDescriptor[type] + return { type, filename } + } +} + +module.exports = ConfigTransform diff --git a/packages/@vue/cli/lib/Generator.js b/packages/@vue/cli/lib/Generator.js index 4783a8166d..0d1357cd8e 100644 --- a/packages/@vue/cli/lib/Generator.js +++ b/packages/@vue/cli/lib/Generator.js @@ -3,10 +3,10 @@ const debug = require('debug') const GeneratorAPI = require('./GeneratorAPI') const sortObject = require('./util/sortObject') const writeFileTree = require('./util/writeFileTree') -const configTransforms = require('./util/configTransforms') const normalizeFilePaths = require('./util/normalizeFilePaths') const injectImportsAndOptions = require('./util/injectImportsAndOptions') const { toShortPluginId, matchesPluginId } = require('@vue/cli-shared-utils') +const ConfigTransform = require('./ConfigTransform') const logger = require('@vue/cli-shared-utils/lib/logger') const logTypes = { @@ -17,6 +17,46 @@ const logTypes = { error: logger.error } +const defaultConfigTransforms = { + babel: new ConfigTransform({ + file: { + js: ['babel.config.js'] + } + }), + postcss: new ConfigTransform({ + file: { + js: ['.postcssrc.js'], + json: ['.postcssrc.json', '.postcssrc'], + yaml: ['.postcssrc.yaml', '.postcssrc.yml'] + } + }), + eslintConfig: new ConfigTransform({ + file: { + js: ['.eslintrc.js'], + json: ['.eslintrc', '.eslintrc.json'], + yaml: ['.eslintrc.yaml', '.eslintrc.yml'] + } + }), + jest: new ConfigTransform({ + file: { + js: ['jest.config.js'] + } + }), + browserslist: new ConfigTransform({ + file: { + lines: ['.browserslistrc'] + } + }) +} + +const reservedConfigTransforms = { + vue: new ConfigTransform({ + file: { + js: ['vue.config.js'] + } + }) +} + module.exports = class Generator { constructor (context, { pkg = {}, @@ -32,8 +72,10 @@ module.exports = class Generator { this.imports = {} this.rootOptions = {} this.completeCbs = completeCbs + this.configTransforms = {} + this.defaultConfigTransforms = defaultConfigTransforms + this.reservedConfigTransforms = reservedConfigTransforms this.invoking = invoking - // for conflict resolution this.depSources = {} // virtual file tree @@ -70,6 +112,11 @@ module.exports = class Generator { } extractConfigFiles (extractAll, checkExisting) { + const configTransforms = Object.assign({}, + defaultConfigTransforms, + this.configTransforms, + reservedConfigTransforms + ) const extract = key => { if ( configTransforms[key] && @@ -78,8 +125,8 @@ module.exports = class Generator { !this.originalPkg[key] ) { const value = this.pkg[key] - const transform = configTransforms[key] - const res = transform( + const configTransform = configTransforms[key] + const res = configTransform.transform( value, checkExisting, this.files, diff --git a/packages/@vue/cli/lib/GeneratorAPI.js b/packages/@vue/cli/lib/GeneratorAPI.js index afa1c836c5..044a5c39ab 100644 --- a/packages/@vue/cli/lib/GeneratorAPI.js +++ b/packages/@vue/cli/lib/GeneratorAPI.js @@ -8,7 +8,8 @@ const isBinary = require('isbinaryfile') const yaml = require('yaml-front-matter') const mergeDeps = require('./util/mergeDeps') const stringifyJS = require('./util/stringifyJS') -const { getPluginLink, toShortPluginId } = require('@vue/cli-shared-utils') +const { warn, getPluginLink, toShortPluginId } = require('@vue/cli-shared-utils') +const ConfigTransform = require('./ConfigTransform') const isString = val => typeof val === 'string' const isFunction = val => typeof val === 'function' @@ -81,6 +82,38 @@ class GeneratorAPI { return this.generator.hasPlugin(id) } + /** + * Configure how config files are extracted. + * + * @param {string} key - Config key in package.json + * @param {object} options - Options + * @param {object} options.file - File descriptor + * Used to search for existing file. + * Each key is a file type (possible values: ['js', 'json', 'yaml', 'lines']). + * The value is a list of filenames. + * Example: + * { + * js: ['.eslintrc.js'], + * json: ['.eslintrc.json', '.eslintrc'] + * } + * By default, the first filename will be used to create the config file. + */ + addConfigTransform (key, options) { + const hasReserved = Object.keys(this.generator.reservedConfigTransforms).includes(key) + if ( + hasReserved || + !options || + !options.file + ) { + if (hasReserved) { + warn(`Reserved config transform '${key}'`) + } + return + } + + this.generator.configTransforms[key] = new ConfigTransform(options) + } + /** * Extend the package.json of the project. * Nested fields are deep-merged unless `{ merge: false }` is passed. diff --git a/packages/@vue/cli/lib/util/configTransforms.js b/packages/@vue/cli/lib/util/configTransforms.js index 5cc961b9a9..6ebf8a6159 100644 --- a/packages/@vue/cli/lib/util/configTransforms.js +++ b/packages/@vue/cli/lib/util/configTransforms.js @@ -5,107 +5,61 @@ const merge = require('deepmerge') const isObject = val => val && typeof val === 'object' -function makeJSTransform (filename) { - return function transformToJS (value, checkExisting, files, context) { - if (checkExisting && files[filename]) { - // Merge data - let changedData = {} - try { - const originalData = loadModule(filename, context, true) - // We merge only the modified keys - Object.keys(value).forEach(key => { - const originalValue = originalData[key] - const newValue = value[key] - if (Array.isArray(newValue)) { - changedData[key] = newValue - } else if (isObject(originalValue) && isObject(newValue)) { - changedData[key] = merge(originalValue, newValue) - } else { - changedData[key] = newValue - } - }) - } catch (e) { - changedData = value - } - // Write - return { - filename, - content: extendJSConfig(changedData, files[filename]) - } +const transformJS = { + read: ({ filename, context }) => { + try { + return loadModule(filename, context, true) + } catch (e) { + return null + } + }, + write: ({ value, existing, source }) => { + if (existing) { + // We merge only the modified keys + const changedData = {} + Object.keys(value).forEach(key => { + const originalValue = existing[key] + const newValue = value[key] + if (Array.isArray(newValue)) { + changedData[key] = newValue + } else if (isObject(originalValue) && isObject(newValue)) { + changedData[key] = merge(originalValue, newValue) + } else { + changedData[key] = newValue + } + }) + return extendJSConfig(changedData, source) } else { - return { - filename, - content: `module.exports = ${stringifyJS(value, null, 2)}` - } + return `module.exports = ${stringifyJS(value, null, 2)}` } } } -function makeJSONTransform (filename) { - return function transformToJSON (value, checkExisting, files) { - let existing = {} - if (checkExisting && files[filename]) { - existing = JSON.parse(files[filename]) - } - value = merge(existing, value) - return { - filename, - content: JSON.stringify(value, null, 2) - } - } +const transformJSON = { + read: ({ source }) => JSON.parse(source), + write: ({ value, existing }) => JSON.stringify(merge(existing, value), null, 2) } -function makeMutliExtensionJSONTransform (filename, preferJS) { - return function transformToMultiExtensions (value, checkExisting, files, context) { - function defaultTransform () { - if (preferJS) { - return makeJSTransform(`${filename}.js`)(value, false, files, context) - } else { - return makeJSONTransform(filename)(value, false, files) - } - } - - if (!checkExisting) { - return defaultTransform() - } - - if (files[filename]) { - return makeJSONTransform(filename)(value, checkExisting, files) - } else if (files[`${filename}.json`]) { - return makeJSONTransform(`${filename}.json`)(value, checkExisting, files) - } else if (files[`${filename}.js`]) { - return makeJSTransform(`${filename}.js`)(value, checkExisting, files, context) - } else if (files[`${filename}.yaml`]) { - return transformYAML(value, `${filename}.yaml`, files[`${filename}.yaml`]) - } else if (files[`${filename}.yml`]) { - return transformYAML(value, `${filename}.yml`, files[`${filename}.yml`]) - } else { - return defaultTransform() - } - } +const transformYAML = { + read: ({ source }) => require('js-yaml').safeLoad(source), + write: ({ value, existing }) => require('js-yaml').safeDump(merge(existing, value)) } -function transformYAML (value, filename, source) { - const yaml = require('js-yaml') - const existing = yaml.safeLoad(source) - return { - filename, - content: yaml.safeDump(merge(existing, value)) - } -} - -function transformBrowserslist (value, filename, source) { - return { - filename: `.browserslistrc`, - content: value.join('\n') +const transformLines = { + read: ({ source }) => source.split('\n'), + write: ({ value, existing }) => { + if (existing) { + value = existing.concat(value) + // Dedupe + value = value.filter((item, index) => value.indexOf(item) === index) + } + return value.join('\n') } } module.exports = { - vue: makeJSTransform('vue.config.js'), - babel: makeJSTransform('babel.config.js'), - postcss: makeMutliExtensionJSONTransform('.postcssrc', true), - eslintConfig: makeMutliExtensionJSONTransform('.eslintrc', true), - jest: makeJSTransform('jest.config.js'), - browserslist: transformBrowserslist + js: transformJS, + json: transformJSON, + yaml: transformYAML, + lines: transformLines }