Skip to content

feat(generator): allow plugins to modify how configs are extracted #1130

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jul 11, 2018
1 change: 1 addition & 0 deletions packages/@vue/cli-plugin-eslint/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ module.exports = api => {
eslint: {
js: ['.eslintrc.js'],
json: ['.eslintrc', '.eslintrc.json'],
yaml: ['.eslintrc.yaml', '.eslintrc.yml'],
package: 'eslintConfig'
},
vue: {
Expand Down
2 changes: 1 addition & 1 deletion packages/@vue/cli-plugin-pwa/lib/HtmlPwaPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
92 changes: 92 additions & 0 deletions packages/@vue/cli/__tests__/Generator.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
65 changes: 65 additions & 0 deletions packages/@vue/cli/lib/ConfigTransform.js
Original file line number Diff line number Diff line change
@@ -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
55 changes: 51 additions & 4 deletions packages/@vue/cli/lib/Generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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 = {},
Expand All @@ -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
Expand Down Expand Up @@ -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] &&
Expand All @@ -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,
Expand Down
35 changes: 34 additions & 1 deletion packages/@vue/cli/lib/GeneratorAPI.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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.
Expand Down
Loading