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:
Stephen Lam
2018-07-11 10:52:57 -07:00
committed by Guillaume Chau
parent 8eb7fc3a97
commit e393be733d
7 changed files with 288 additions and 96 deletions
+1
View File
@@ -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: {
+65
View File
@@ -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
+51 -4
View File
@@ -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,
+34 -1
View File
@@ -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.
+44 -90
View File
@@ -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
}