mirror of
https://github.com/vuejs/vue-cli.git
synced 2026-05-12 14:58:26 -05:00
feat(generator): allow plugins to modify how configs are extracted (#1130)
* feat(generator): allow plugins to modify how configs are extracted * refactor(cli): change addConfigTransform parameters Allow plugin author to set config 'descriptions' instead of implementing their own transform functions. * fix(cli): fix missed issues from changing types from array to set * fix: use reserved config transforms to check in API * fix: lines dedupe
This commit is contained in:
committed by
Guillaume Chau
parent
8eb7fc3a97
commit
e393be733d
@@ -11,6 +11,7 @@ module.exports = api => {
|
||||
eslint: {
|
||||
js: ['.eslintrc.js'],
|
||||
json: ['.eslintrc', '.eslintrc.json'],
|
||||
yaml: ['.eslintrc.yaml', '.eslintrc.yml'],
|
||||
package: 'eslintConfig'
|
||||
},
|
||||
vue: {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user