mirror of
https://github.com/vuejs/vue-cli.git
synced 2026-04-23 21:40:00 -05:00
feat(ui): config improvements (#1487)
BREAKING CHANGES:
- The configuration API has changed.
- The `files` options now accept an object of different config files:
```js
api.describeConfig({
/* ... */
// All possible files for this config
files: {
// eslintrc.js
eslint: {
js: ['.eslintrc.js'],
json: ['.eslintrc', '.eslintrc.json'],
// Will read from `package.json`
package: 'eslintConfig'
},
// vue.config.js
vue: {
js: ['vue.config.js']
}
},
})
```
- The `onWrite` api has changed: `setData` and `assignData` have now `fileId` as the first argument:
```js
api.describeConfig({
onWrite: async ({ api, prompts }) => {
const eslintData = {}
const vueData = {}
for (const prompt of prompts) {
// eslintrc
if (prompt.id.indexOf('vue/') === 0) {
eslintData[`rules.${prompt.id}`] = await api.getAnswer(prompt.id, JSON.parse)
} else {
// vue.config.js
vueData[prompt.id] = await api.getAnswer(prompt.id)
}
}
api.setData('eslint', eslintData)
api.setData('vue', vueData)
}
})
```
Other changes
- Config tabs (optional):
```js
api.describeConfig({
/* ... */
onRead: ({ data, cwd }) => ({
tabs: [
{
id: 'tab1',
label: 'My tab',
// Optional
icon: 'application_settings',
prompts: [
// Prompt objects
]
},
{
id: 'tab2',
label: 'My other tab',
prompts: [
// Prompt objects
]
}
]
})
})
```
This commit is contained in:
@@ -6,202 +6,206 @@ module.exports = api => {
|
||||
description: 'eslint.config.eslint.description',
|
||||
link: 'https://github.com/vuejs/eslint-plugin-vue',
|
||||
files: {
|
||||
json: ['.eslintrc', '.eslintrc.json'],
|
||||
js: ['.eslintrc.js'],
|
||||
package: 'eslintConfig'
|
||||
eslint: {
|
||||
js: ['.eslintrc.js'],
|
||||
json: ['.eslintrc', '.eslintrc.json'],
|
||||
package: 'eslintConfig'
|
||||
},
|
||||
vue: {
|
||||
js: ['vue.config.js']
|
||||
}
|
||||
},
|
||||
onRead: ({ data }) => ({
|
||||
prompts: [
|
||||
tabs: [
|
||||
{
|
||||
name: 'vue/attribute-hyphenation',
|
||||
type: 'list',
|
||||
message: 'Attribute hyphenation',
|
||||
group: 'eslint.config.eslint.groups.strongly-recommended',
|
||||
description: 'Enforce attribute naming style in template (`my-prop` or `myProp`)',
|
||||
link: 'https://github.com/vuejs/eslint-plugin-vue/blob/master/docs/rules/attribute-hyphenation.md',
|
||||
default: JSON.stringify('off'),
|
||||
choices: [
|
||||
id: 'vue',
|
||||
label: 'eslint.config.eslint.vue.label',
|
||||
prompts: [
|
||||
{
|
||||
name: 'Off',
|
||||
value: JSON.stringify('off')
|
||||
name: 'vue/attribute-hyphenation',
|
||||
type: 'list',
|
||||
message: 'Attribute hyphenation',
|
||||
group: 'eslint.config.eslint.groups.strongly-recommended',
|
||||
description: 'Enforce attribute naming style in template (`my-prop` or `myProp`)',
|
||||
link: 'https://github.com/vuejs/eslint-plugin-vue/blob/master/docs/rules/attribute-hyphenation.md',
|
||||
default: JSON.stringify('off'),
|
||||
choices: [
|
||||
{
|
||||
name: 'Off',
|
||||
value: JSON.stringify('off')
|
||||
},
|
||||
{
|
||||
name: 'Never',
|
||||
value: JSON.stringify(['error', 'never'])
|
||||
},
|
||||
{
|
||||
name: 'Always',
|
||||
value: JSON.stringify(['error', 'always'])
|
||||
}
|
||||
],
|
||||
value: data.eslint && data.eslint.rules && JSON.stringify(data.eslint.rules['vue/attribute-hyphenation'])
|
||||
},
|
||||
{
|
||||
name: 'Never',
|
||||
value: JSON.stringify(['error', 'never'])
|
||||
name: 'vue/html-end-tags',
|
||||
type: 'confirm',
|
||||
message: 'Template end tags style',
|
||||
group: 'eslint.config.eslint.groups.strongly-recommended',
|
||||
description: 'End tag on Void elements, end tags and self-closing opening tags',
|
||||
link: 'https://github.com/vuejs/eslint-plugin-vue/blob/master/docs/rules/html-end-tags.md',
|
||||
default: false,
|
||||
value: data.eslint && data.eslint.rules && data.eslint.rules['vue/html-end-tags'] === 'error',
|
||||
filter: input => JSON.stringify(input ? 'error' : 'off'),
|
||||
transformer: input => input === JSON.stringify('error')
|
||||
},
|
||||
{
|
||||
name: 'Always',
|
||||
value: JSON.stringify(['error', 'always'])
|
||||
name: 'vue/html-indent',
|
||||
type: 'list',
|
||||
message: 'Template indentation',
|
||||
group: 'eslint.config.eslint.groups.strongly-recommended',
|
||||
description: 'Enforce indentation in template',
|
||||
link: 'https://github.com/vuejs/eslint-plugin-vue/blob/master/docs/rules/html-indent.md',
|
||||
default: JSON.stringify('off'),
|
||||
choices: [
|
||||
{
|
||||
name: 'Off',
|
||||
value: JSON.stringify('off')
|
||||
},
|
||||
{
|
||||
name: 'Tabs',
|
||||
value: JSON.stringify(['error', 'tab'])
|
||||
},
|
||||
{
|
||||
name: '2 spaces',
|
||||
value: JSON.stringify(['error', 2])
|
||||
},
|
||||
{
|
||||
name: '4 spaces',
|
||||
value: JSON.stringify(['error', 4])
|
||||
},
|
||||
{
|
||||
name: '8 spaces',
|
||||
value: JSON.stringify(['error', 8])
|
||||
}
|
||||
],
|
||||
value: data.eslint && data.eslint.rules && JSON.stringify(data.eslint.rules['vue/html-indent'])
|
||||
},
|
||||
{
|
||||
name: 'vue/html-self-closing',
|
||||
type: 'confirm',
|
||||
message: 'Template tag self-closing style',
|
||||
group: 'eslint.config.eslint.groups.strongly-recommended',
|
||||
description: 'Self-close any component or non-Void element tags',
|
||||
link: 'https://github.com/vuejs/eslint-plugin-vue/blob/master/docs/rules/html-self-closing.md',
|
||||
default: false,
|
||||
value: data.eslint && data.eslint.rules && data.eslint.rules['vue/html-self-closing'] === 'error',
|
||||
filter: input => JSON.stringify(input ? 'error' : 'off'),
|
||||
transformer: input => input === JSON.stringify('error')
|
||||
},
|
||||
{
|
||||
name: 'vue/require-default-prop',
|
||||
type: 'confirm',
|
||||
message: 'Require default in required props',
|
||||
group: 'eslint.config.eslint.groups.strongly-recommended',
|
||||
description: 'This rule requires default value to be set for each props that are not marked as `required`',
|
||||
link: 'https://github.com/vuejs/eslint-plugin-vue/blob/master/docs/rules/require-default-prop.md',
|
||||
default: false,
|
||||
value: data.eslint && data.eslint.rules && data.eslint.rules['vue/require-default-prop'] === 'error',
|
||||
filter: input => JSON.stringify(input ? 'error' : 'off'),
|
||||
transformer: input => input === JSON.stringify('error')
|
||||
},
|
||||
{
|
||||
name: 'vue/require-prop-types',
|
||||
type: 'confirm',
|
||||
message: 'Require types for props',
|
||||
group: 'eslint.config.eslint.groups.strongly-recommended',
|
||||
description: 'In committed code, prop definitions should always be as detailed as possible, specifying at least type(s)',
|
||||
link: 'https://github.com/vuejs/eslint-plugin-vue/blob/master/docs/rules/require-prop-types.md',
|
||||
default: false,
|
||||
value: data.eslint && data.eslint.rules && data.eslint.rules['vue/require-prop-types'] === 'error',
|
||||
filter: input => JSON.stringify(input ? 'error' : 'off'),
|
||||
transformer: input => input === JSON.stringify('error')
|
||||
},
|
||||
{
|
||||
name: 'vue/attributes-order',
|
||||
type: 'confirm',
|
||||
message: 'Attribute order',
|
||||
group: 'eslint.config.eslint.groups.recommended',
|
||||
description: 'This rule aims to enforce ordering of component attributes (the default order is specified in the Vue style guide)',
|
||||
link: 'https://github.com/vuejs/eslint-plugin-vue/blob/master/docs/rules/attributes-order.md',
|
||||
default: false,
|
||||
value: data.eslint && data.eslint.rules && data.eslint.rules['vue/attributes-order'] === 'error',
|
||||
filter: input => JSON.stringify(input ? 'error' : 'off'),
|
||||
transformer: input => input === JSON.stringify('error')
|
||||
},
|
||||
{
|
||||
name: 'vue/html-quotes',
|
||||
type: 'list',
|
||||
message: 'Attribute quote style',
|
||||
group: 'eslint.config.eslint.groups.recommended',
|
||||
description: 'Enforce style of the attribute quotes in templates',
|
||||
link: 'https://github.com/vuejs/eslint-plugin-vue/blob/master/docs/rules/html-quotes.md',
|
||||
default: JSON.stringify('off'),
|
||||
choices: [
|
||||
{
|
||||
name: 'Off',
|
||||
value: JSON.stringify('off')
|
||||
},
|
||||
{
|
||||
name: 'Double quotes',
|
||||
value: JSON.stringify(['error', 'double'])
|
||||
},
|
||||
{
|
||||
name: 'Single quotes',
|
||||
value: JSON.stringify(['error', 'single'])
|
||||
}
|
||||
],
|
||||
value: data.eslint && data.eslint.rules && JSON.stringify(data.eslint.rules['vue/html-quotes'])
|
||||
},
|
||||
{
|
||||
name: 'vue/order-in-components',
|
||||
type: 'confirm',
|
||||
message: 'Component options order',
|
||||
group: 'eslint.config.eslint.groups.recommended',
|
||||
description: 'This rule aims to enforce ordering of component options (the default order is specified in the Vue style guide)',
|
||||
link: 'https://github.com/vuejs/eslint-plugin-vue/blob/master/docs/rules/order-in-components.md',
|
||||
default: false,
|
||||
value: data.eslint && data.eslint.rules && data.eslint.rules['vue/order-in-components'] === 'error',
|
||||
filter: input => JSON.stringify(input ? 'error' : 'off'),
|
||||
transformer: input => input === JSON.stringify('error')
|
||||
}
|
||||
],
|
||||
value: data.rules && JSON.stringify(data.rules['vue/attribute-hyphenation'])
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'vue/html-end-tags',
|
||||
type: 'confirm',
|
||||
message: 'Template end tags style',
|
||||
group: 'eslint.config.eslint.groups.strongly-recommended',
|
||||
description: 'End tag on Void elements, end tags and self-closing opening tags',
|
||||
link: 'https://github.com/vuejs/eslint-plugin-vue/blob/master/docs/rules/html-end-tags.md',
|
||||
default: false,
|
||||
value: data.rules && data.rules['vue/html-end-tags'] === 'error',
|
||||
filter: input => JSON.stringify(input ? 'error' : 'off'),
|
||||
transformer: input => input === JSON.stringify('error')
|
||||
},
|
||||
{
|
||||
name: 'vue/html-indent',
|
||||
type: 'list',
|
||||
message: 'Template indentation',
|
||||
group: 'eslint.config.eslint.groups.strongly-recommended',
|
||||
description: 'Enforce indentation in template',
|
||||
link: 'https://github.com/vuejs/eslint-plugin-vue/blob/master/docs/rules/html-indent.md',
|
||||
default: JSON.stringify('off'),
|
||||
choices: [
|
||||
id: 'extra',
|
||||
label: 'eslint.config.eslint.extra.label',
|
||||
prompts: [
|
||||
{
|
||||
name: 'Off',
|
||||
value: JSON.stringify('off')
|
||||
},
|
||||
{
|
||||
name: 'Tabs',
|
||||
value: JSON.stringify(['error', 'tab'])
|
||||
},
|
||||
{
|
||||
name: '2 spaces',
|
||||
value: JSON.stringify(['error', 2])
|
||||
},
|
||||
{
|
||||
name: '4 spaces',
|
||||
value: JSON.stringify(['error', 4])
|
||||
},
|
||||
{
|
||||
name: '8 spaces',
|
||||
value: JSON.stringify(['error', 8])
|
||||
name: 'lintOnSave',
|
||||
type: 'confirm',
|
||||
message: 'eslint.config.eslint.extra.lintOnSave.message',
|
||||
description: 'eslint.config.eslint.extra.lintOnSave.description',
|
||||
link: 'https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint#configuration',
|
||||
default: true,
|
||||
value: data.vue && data.vue.lintOnSave
|
||||
}
|
||||
],
|
||||
value: data.rules && JSON.stringify(data.rules['vue/html-indent'])
|
||||
},
|
||||
{
|
||||
name: 'vue/html-self-closing',
|
||||
type: 'confirm',
|
||||
message: 'Template tag self-closing style',
|
||||
group: 'eslint.config.eslint.groups.strongly-recommended',
|
||||
description: 'Self-close any component or non-Void element tags',
|
||||
link: 'https://github.com/vuejs/eslint-plugin-vue/blob/master/docs/rules/html-self-closing.md',
|
||||
default: false,
|
||||
value: data.rules && data.rules['vue/html-self-closing'] === 'error',
|
||||
filter: input => JSON.stringify(input ? 'error' : 'off'),
|
||||
transformer: input => input === JSON.stringify('error')
|
||||
},
|
||||
{
|
||||
name: 'vue/require-default-prop',
|
||||
type: 'confirm',
|
||||
message: 'Require default in required props',
|
||||
group: 'eslint.config.eslint.groups.strongly-recommended',
|
||||
description: 'This rule requires default value to be set for each props that are not marked as `required`',
|
||||
link: 'https://github.com/vuejs/eslint-plugin-vue/blob/master/docs/rules/require-default-prop.md',
|
||||
default: false,
|
||||
value: data.rules && data.rules['vue/require-default-prop'] === 'error',
|
||||
filter: input => JSON.stringify(input ? 'error' : 'off'),
|
||||
transformer: input => input === JSON.stringify('error')
|
||||
},
|
||||
{
|
||||
name: 'vue/require-prop-types',
|
||||
type: 'confirm',
|
||||
message: 'Require types for props',
|
||||
group: 'eslint.config.eslint.groups.strongly-recommended',
|
||||
description: 'In committed code, prop definitions should always be as detailed as possible, specifying at least type(s)',
|
||||
link: 'https://github.com/vuejs/eslint-plugin-vue/blob/master/docs/rules/require-prop-types.md',
|
||||
default: false,
|
||||
value: data.rules && data.rules['vue/require-prop-types'] === 'error',
|
||||
filter: input => JSON.stringify(input ? 'error' : 'off'),
|
||||
transformer: input => input === JSON.stringify('error')
|
||||
},
|
||||
{
|
||||
name: 'vue/attributes-order',
|
||||
type: 'confirm',
|
||||
message: 'Attribute order',
|
||||
group: 'eslint.config.eslint.groups.recommended',
|
||||
description: 'This rule aims to enforce ordering of component attributes (the default order is specified in the Vue style guide)',
|
||||
link: 'https://github.com/vuejs/eslint-plugin-vue/blob/master/docs/rules/attributes-order.md',
|
||||
default: false,
|
||||
value: data.rules && data.rules['vue/attributes-order'] === 'error',
|
||||
filter: input => JSON.stringify(input ? 'error' : 'off'),
|
||||
transformer: input => input === JSON.stringify('error')
|
||||
},
|
||||
{
|
||||
name: 'vue/html-quotes',
|
||||
type: 'list',
|
||||
message: 'Attribute quote style',
|
||||
group: 'eslint.config.eslint.groups.recommended',
|
||||
description: 'Enforce style of the attribute quotes in templates',
|
||||
link: 'https://github.com/vuejs/eslint-plugin-vue/blob/master/docs/rules/html-quotes.md',
|
||||
default: JSON.stringify('off'),
|
||||
choices: [
|
||||
{
|
||||
name: 'Off',
|
||||
value: JSON.stringify('off')
|
||||
},
|
||||
{
|
||||
name: 'Double quotes',
|
||||
value: JSON.stringify(['error', 'double'])
|
||||
},
|
||||
{
|
||||
name: 'Single quotes',
|
||||
value: JSON.stringify(['error', 'single'])
|
||||
}
|
||||
],
|
||||
value: data.rules && JSON.stringify(data.rules['vue/html-quotes'])
|
||||
},
|
||||
{
|
||||
name: 'vue/order-in-components',
|
||||
type: 'confirm',
|
||||
message: 'Component options order',
|
||||
group: 'eslint.config.eslint.groups.recommended',
|
||||
description: 'This rule aims to enforce ordering of component options (the default order is specified in the Vue style guide)',
|
||||
link: 'https://github.com/vuejs/eslint-plugin-vue/blob/master/docs/rules/order-in-components.md',
|
||||
default: false,
|
||||
value: data.rules && data.rules['vue/order-in-components'] === 'error',
|
||||
filter: input => JSON.stringify(input ? 'error' : 'off'),
|
||||
transformer: input => input === JSON.stringify('error')
|
||||
]
|
||||
}
|
||||
]
|
||||
}),
|
||||
onWrite: async ({ api, prompts }) => {
|
||||
const result = {}
|
||||
const eslintData = {}
|
||||
const vueData = {}
|
||||
for (const prompt of prompts) {
|
||||
result[`rules.${prompt.id}`] = await api.getAnswer(prompt.id, JSON.parse)
|
||||
}
|
||||
api.setData(result)
|
||||
}
|
||||
})
|
||||
|
||||
api.describeConfig({
|
||||
id: 'eslintrc-config',
|
||||
name: 'ESLint extra',
|
||||
description: 'eslint.config.eslint-extra.description',
|
||||
link: 'https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint#configuration',
|
||||
files: {
|
||||
js: ['vue.config.js']
|
||||
},
|
||||
onRead: ({ data }) => ({
|
||||
prompts: [
|
||||
{
|
||||
name: 'lintOnSave',
|
||||
type: 'confirm',
|
||||
message: 'eslint.config.eslint-extra.lintOnSave.message',
|
||||
description: 'eslint.config.eslint-extra.lintOnSave.description',
|
||||
link: 'https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint#configuration',
|
||||
default: true,
|
||||
value: data.lintOnSave
|
||||
// eslintrc
|
||||
if (prompt.id.indexOf('vue/') === 0) {
|
||||
eslintData[`rules.${prompt.id}`] = await api.getAnswer(prompt.id, JSON.parse)
|
||||
} else {
|
||||
// vue.config.js
|
||||
vueData[prompt.id] = await api.getAnswer(prompt.id)
|
||||
}
|
||||
]
|
||||
}),
|
||||
onWrite: async ({ api, prompts }) => {
|
||||
const result = {}
|
||||
for (const prompt of prompts) {
|
||||
result[prompt.id] = await api.getAnswer(prompt.id)
|
||||
}
|
||||
api.setData(result)
|
||||
api.setData('eslint', eslintData)
|
||||
api.setData('vue', vueData)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,30 +1,3 @@
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
|
||||
function readAppManifest (cwd) {
|
||||
const manifestPath = path.join(cwd, 'public/manifest.json')
|
||||
if (fs.existsSync(manifestPath)) {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(manifestPath, { encoding: 'utf8' }))
|
||||
} catch (e) {
|
||||
console.log(`Can't read JSON in ${manifestPath}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateAppManifest (cwd, handler) {
|
||||
const manifestPath = path.join(cwd, 'public/manifest.json')
|
||||
if (fs.existsSync(manifestPath)) {
|
||||
try {
|
||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, { encoding: 'utf8' }))
|
||||
handler(manifest)
|
||||
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), { encoding: 'utf8' })
|
||||
} catch (e) {
|
||||
console.log(`Can't update JSON in ${manifestPath}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = api => {
|
||||
// Config file
|
||||
api.describeConfig({
|
||||
@@ -33,10 +6,14 @@ module.exports = api => {
|
||||
description: 'pwa.config.pwa.description',
|
||||
link: 'https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-pwa#configuration',
|
||||
files: {
|
||||
js: ['vue.config.js']
|
||||
vue: {
|
||||
js: ['vue.config.js']
|
||||
},
|
||||
manifest: {
|
||||
json: ['public/manifest.json']
|
||||
}
|
||||
},
|
||||
onRead: ({ data, cwd }) => {
|
||||
const manifest = readAppManifest(cwd)
|
||||
return {
|
||||
prompts: [
|
||||
{
|
||||
@@ -46,7 +23,7 @@ module.exports = api => {
|
||||
description: 'pwa.config.pwa.workboxPluginMode.description',
|
||||
link: 'https://developers.google.com/web/tools/workbox/modules/workbox-webpack-plugin#which_plugin_to_use',
|
||||
default: 'GenerateSW',
|
||||
value: data.pwa && data.pwa.workboxPluginMode,
|
||||
value: data.vue && data.vue.pwa && data.vue.pwa.workboxPluginMode,
|
||||
choices: [
|
||||
{
|
||||
name: 'GenerateSW',
|
||||
@@ -63,7 +40,7 @@ module.exports = api => {
|
||||
type: 'input',
|
||||
message: 'pwa.config.pwa.name.message',
|
||||
description: 'pwa.config.pwa.name.description',
|
||||
value: data.pwa && data.pwa.name
|
||||
value: data.vue && data.vue.pwa && data.vue.pwa.name
|
||||
},
|
||||
{
|
||||
name: 'themeColor',
|
||||
@@ -71,7 +48,7 @@ module.exports = api => {
|
||||
message: 'pwa.config.pwa.themeColor.message',
|
||||
description: 'pwa.config.pwa.themeColor.description',
|
||||
default: '#4DBA87',
|
||||
value: data.pwa && data.pwa.themeColor
|
||||
value: data.vue && data.vue.pwa && data.vue.pwa.themeColor
|
||||
},
|
||||
{
|
||||
name: 'backgroundColor',
|
||||
@@ -79,7 +56,7 @@ module.exports = api => {
|
||||
message: 'pwa.config.pwa.backgroundColor.message',
|
||||
description: 'pwa.config.pwa.backgroundColor.description',
|
||||
default: '#000000',
|
||||
value: manifest && manifest.background_color,
|
||||
value: data.manifest && data.manifest.background_color,
|
||||
skipSave: true
|
||||
},
|
||||
{
|
||||
@@ -88,7 +65,7 @@ module.exports = api => {
|
||||
message: 'pwa.config.pwa.msTileColor.message',
|
||||
description: 'pwa.config.pwa.msTileColor.description',
|
||||
default: '#000000',
|
||||
value: data.pwa && data.pwa.msTileColor
|
||||
value: data.vue && data.vue.pwa && data.vue.pwa.msTileColor
|
||||
},
|
||||
{
|
||||
name: 'appleMobileWebAppStatusBarStyle',
|
||||
@@ -96,7 +73,7 @@ module.exports = api => {
|
||||
message: 'pwa.config.pwa.appleMobileWebAppStatusBarStyle.message',
|
||||
description: 'pwa.config.pwa.appleMobileWebAppStatusBarStyle.description',
|
||||
default: 'default',
|
||||
value: data.pwa && data.pwa.appleMobileWebAppStatusBarStyle
|
||||
value: data.vue && data.vue.pwa && data.vue.pwa.appleMobileWebAppStatusBarStyle
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -106,29 +83,29 @@ module.exports = api => {
|
||||
for (const prompt of prompts.filter(p => !p.raw.skipSave)) {
|
||||
result[`pwa.${prompt.id}`] = await api.getAnswer(prompt.id)
|
||||
}
|
||||
api.setData(result)
|
||||
api.setData('vue', result)
|
||||
|
||||
// Update app manifest
|
||||
|
||||
const name = result['pwa.name']
|
||||
if (name) {
|
||||
updateAppManifest(cwd, manifest => {
|
||||
manifest.name = name
|
||||
manifest.short_name = name
|
||||
api.setData('manifest', {
|
||||
name,
|
||||
short_name: name
|
||||
})
|
||||
}
|
||||
|
||||
const themeColor = result['pwa.themeColor']
|
||||
if (themeColor) {
|
||||
updateAppManifest(cwd, manifest => {
|
||||
manifest.theme_color = themeColor
|
||||
api.setData('manifest', {
|
||||
theme_color: themeColor
|
||||
})
|
||||
}
|
||||
|
||||
const backgroundColor = await api.getAnswer('backgroundColor')
|
||||
if (backgroundColor) {
|
||||
updateAppManifest(cwd, manifest => {
|
||||
manifest.background_color = backgroundColor
|
||||
api.setData('manifest', {
|
||||
background_color: backgroundColor
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,7 +313,8 @@
|
||||
"actions": {
|
||||
"cancel": "Cancel changes",
|
||||
"save": "Save changes",
|
||||
"more-info": "More info"
|
||||
"more-info": "More info",
|
||||
"refresh": "Refresh"
|
||||
}
|
||||
},
|
||||
"project-tasks": {
|
||||
@@ -420,13 +421,16 @@
|
||||
"groups": {
|
||||
"strongly-recommended": "Strongly recommended",
|
||||
"recommended": "Recommended"
|
||||
}
|
||||
},
|
||||
"eslint-extra": {
|
||||
"description": "Extra ESLint settings",
|
||||
"lintOnSave": {
|
||||
"message": "Lint on save",
|
||||
"description": "Automatically lint source files when saved"
|
||||
},
|
||||
"vue": {
|
||||
"label": "Vue"
|
||||
},
|
||||
"extra": {
|
||||
"label": "Extra",
|
||||
"lintOnSave": {
|
||||
"message": "Lint on save",
|
||||
"description": "Automatically lint source files when saved"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div class="configuration-tab">
|
||||
<PromptsList
|
||||
:prompts="visiblePrompts"
|
||||
@answer="answerPrompt"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Prompts from '../mixins/Prompts'
|
||||
|
||||
import CONFIGURATION from '../graphql/configuration.gql'
|
||||
|
||||
export default {
|
||||
mixins: [
|
||||
Prompts({
|
||||
field: 'tab',
|
||||
query: CONFIGURATION,
|
||||
variables () {
|
||||
return {
|
||||
id: this.configuration.id
|
||||
}
|
||||
},
|
||||
updateQuery (data, prompts) {
|
||||
const result = {}
|
||||
for (const prompt of prompts) {
|
||||
const list = result[prompt.tabId] || (result[prompt.tabId] = [])
|
||||
list.push(prompt)
|
||||
}
|
||||
for (const tabId in result) {
|
||||
data.configuration.tabs[tabId].prompts = result[tabId]
|
||||
}
|
||||
}
|
||||
})
|
||||
],
|
||||
|
||||
props: {
|
||||
configuration: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
|
||||
tab: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -6,12 +6,12 @@ const schema = createSchema(joi => ({
|
||||
description: joi.string(),
|
||||
link: joi.string().uri(),
|
||||
icon: joi.string(),
|
||||
files: joi.object({
|
||||
files: joi.object().pattern(/^/, joi.object({
|
||||
json: joi.array().items(joi.string()),
|
||||
js: joi.array().items(joi.string()),
|
||||
yaml: joi.array().items(joi.string()),
|
||||
package: joi.string()
|
||||
}),
|
||||
})),
|
||||
onRead: joi.func().required(),
|
||||
onWrite: joi.func().required()
|
||||
}))
|
||||
|
||||
@@ -27,21 +27,17 @@ function findOne (id, context) {
|
||||
)
|
||||
}
|
||||
|
||||
function findFile (config, context) {
|
||||
if (!config.files) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (config.files.package) {
|
||||
function findFile (fileDescriptor, context) {
|
||||
if (fileDescriptor.package) {
|
||||
const pkg = folders.readPackage(cwd.get(), context)
|
||||
const data = pkg[config.files.package]
|
||||
const data = pkg[fileDescriptor.package]
|
||||
if (data) {
|
||||
return { type: 'package', path: path.join(cwd.get(), 'package.json') }
|
||||
}
|
||||
}
|
||||
|
||||
for (const type of fileTypes) {
|
||||
const files = config.files[type]
|
||||
const files = fileDescriptor[type]
|
||||
if (files) {
|
||||
for (const file of files) {
|
||||
const resolvedFile = path.resolve(cwd.get(), file)
|
||||
@@ -53,16 +49,12 @@ function findFile (config, context) {
|
||||
}
|
||||
}
|
||||
|
||||
function getDefaultFile (config, context) {
|
||||
if (!config.files) {
|
||||
return null
|
||||
}
|
||||
|
||||
const keys = Object.keys(config.files)
|
||||
function getDefaultFile (fileDescriptor, context) {
|
||||
const keys = Object.keys(fileDescriptor)
|
||||
if (keys.length) {
|
||||
for (const key of keys) {
|
||||
if (key !== 'package') {
|
||||
const file = config.files[key][0]
|
||||
const file = fileDescriptor[key][0]
|
||||
return {
|
||||
type: key,
|
||||
path: path.resolve(cwd.get(), file)
|
||||
@@ -72,65 +64,88 @@ function getDefaultFile (config, context) {
|
||||
}
|
||||
}
|
||||
|
||||
function readData (config, context) {
|
||||
const file = findFile(config, context)
|
||||
config.file = file
|
||||
function readFile (config, fileDescriptor, context) {
|
||||
const file = findFile(fileDescriptor, context)
|
||||
let fileData = {}
|
||||
if (file) {
|
||||
if (file.type === 'package') {
|
||||
const pkg = folders.readPackage(cwd.get(), context)
|
||||
return pkg[config.files.package]
|
||||
fileData = pkg[config.files.package]
|
||||
} else if (file.type === 'js') {
|
||||
return loadModule(file.path, cwd.get(), true)
|
||||
fileData = loadModule(file.path, cwd.get(), true)
|
||||
} else {
|
||||
const rawContent = fs.readFileSync(file.path, { encoding: 'utf8' })
|
||||
if (file.type === 'json') {
|
||||
return JSON.parse(rawContent)
|
||||
fileData = JSON.parse(rawContent)
|
||||
} else if (file.type === 'yaml') {
|
||||
return yaml.safeLoad(rawContent)
|
||||
fileData = yaml.safeLoad(rawContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
return {}
|
||||
return {
|
||||
file,
|
||||
fileData
|
||||
}
|
||||
}
|
||||
|
||||
function readData (config, context) {
|
||||
const data = {}
|
||||
config.foundFiles = {}
|
||||
if (!config.files) return data
|
||||
for (const fileId in config.files) {
|
||||
const fileDescriptor = config.files[fileId]
|
||||
const { file, fileData } = readFile(config, fileDescriptor, context)
|
||||
config.foundFiles[fileId] = file
|
||||
data[fileId] = fileData
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
function writeFile (config, fileId, data, changedFields, context) {
|
||||
const fileDescriptor = config.files[fileId]
|
||||
let file = findFile(fileDescriptor, context)
|
||||
|
||||
if (!file) {
|
||||
file = getDefaultFile(fileDescriptor, context)
|
||||
}
|
||||
|
||||
if (!file) return
|
||||
|
||||
log('Config write', config.id, data, changedFields, file.path)
|
||||
fs.ensureFileSync(file.path)
|
||||
let rawContent
|
||||
if (file.type === 'package') {
|
||||
const pkg = folders.readPackage(cwd.get(), context)
|
||||
pkg[config.files.package] = data
|
||||
rawContent = JSON.stringify(pkg, null, 2)
|
||||
} else {
|
||||
if (file.type === 'json') {
|
||||
rawContent = JSON.stringify(data, null, 2)
|
||||
} else if (file.type === 'yaml') {
|
||||
rawContent = yaml.safeDump(data)
|
||||
} else if (file.type === 'js') {
|
||||
let source = fs.readFileSync(file.path, { encoding: 'utf8' })
|
||||
if (!source.trim()) {
|
||||
rawContent = `module.exports = ${stringifyJS(data, null, 2)}`
|
||||
} else {
|
||||
const changedData = changedFields.reduce((obj, field) => {
|
||||
obj[field] = data[field]
|
||||
return obj
|
||||
}, {})
|
||||
rawContent = extendJSConfig(changedData, source)
|
||||
}
|
||||
}
|
||||
}
|
||||
fs.writeFileSync(file.path, rawContent, { encoding: 'utf8' })
|
||||
}
|
||||
|
||||
function writeData ({ config, data, changedFields }, context) {
|
||||
let file = findFile(config, context)
|
||||
|
||||
if (!file) {
|
||||
file = getDefaultFile(config, context)
|
||||
}
|
||||
|
||||
if (file) {
|
||||
log('Config write', config.id, data, changedFields, file.path)
|
||||
fs.ensureFileSync(file.path)
|
||||
let rawContent
|
||||
if (file.type === 'package') {
|
||||
const pkg = folders.readPackage(cwd.get(), context)
|
||||
pkg[config.files.package] = data
|
||||
rawContent = JSON.stringify(pkg, null, 2)
|
||||
} else {
|
||||
if (file.type === 'json') {
|
||||
rawContent = JSON.stringify(data, null, 2)
|
||||
} else if (file.type === 'yaml') {
|
||||
rawContent = yaml.safeDump(data)
|
||||
} else if (file.type === 'js') {
|
||||
let source = fs.readFileSync(file.path, { encoding: 'utf8' })
|
||||
if (!source.trim()) {
|
||||
rawContent = `module.exports = ${stringifyJS(data, null, 2)}`
|
||||
} else {
|
||||
const changedData = changedFields.reduce((obj, field) => {
|
||||
obj[field] = data[field]
|
||||
return obj
|
||||
}, {})
|
||||
rawContent = extendJSConfig(changedData, source)
|
||||
}
|
||||
}
|
||||
}
|
||||
fs.writeFileSync(file.path, rawContent, { encoding: 'utf8' })
|
||||
for (const fileId in data) {
|
||||
writeFile(config, fileId, data[fileId], changedFields[fileId], context)
|
||||
}
|
||||
}
|
||||
|
||||
async function getPrompts (id, context) {
|
||||
async function getPromptTabs (id, context) {
|
||||
const config = findOne(id, context)
|
||||
if (config) {
|
||||
const data = readData(config, context)
|
||||
@@ -139,17 +154,35 @@ async function getPrompts (id, context) {
|
||||
config,
|
||||
data
|
||||
}
|
||||
|
||||
// API
|
||||
const configData = await config.onRead({
|
||||
cwd: cwd.get(),
|
||||
data
|
||||
})
|
||||
|
||||
let tabs = configData.tabs
|
||||
if (!tabs) {
|
||||
tabs = [
|
||||
{
|
||||
id: '__default',
|
||||
label: 'Default',
|
||||
prompts: configData.prompts
|
||||
}
|
||||
]
|
||||
}
|
||||
await prompts.reset()
|
||||
configData.prompts.forEach(prompts.add)
|
||||
for (const tab of tabs) {
|
||||
tab.prompts = tab.prompts.map(data => prompts.add({
|
||||
...data,
|
||||
tabId: tab.id
|
||||
}))
|
||||
}
|
||||
if (configData.answers) {
|
||||
await prompts.setAnswers(configData.answers)
|
||||
}
|
||||
await prompts.start()
|
||||
return prompts.list()
|
||||
return tabs
|
||||
}
|
||||
return []
|
||||
}
|
||||
@@ -160,32 +193,35 @@ async function save (id, context) {
|
||||
if (current.config === config) {
|
||||
const answers = prompts.getAnswers()
|
||||
let data = clone(current.data)
|
||||
const changedFields = []
|
||||
const changedFields = {}
|
||||
const getChangedFields = fileId => changedFields[fileId] || (changedFields[fileId] = [])
|
||||
|
||||
// API
|
||||
await config.onWrite({
|
||||
prompts: prompts.list(),
|
||||
answers,
|
||||
data: current.data,
|
||||
file: config.file,
|
||||
files: config.foundFiles,
|
||||
cwd: cwd.get(),
|
||||
api: {
|
||||
assignData: newData => {
|
||||
changedFields.push(...Object.keys(newData))
|
||||
Object.assign(data, newData)
|
||||
assignData: (fileId, newData) => {
|
||||
getChangedFields(fileId).push(...Object.keys(newData))
|
||||
Object.assign(data[fileId], newData)
|
||||
},
|
||||
setData: newData => {
|
||||
setData: (fileId, newData) => {
|
||||
Object.keys(newData).forEach(key => {
|
||||
let field = key
|
||||
const dotIndex = key.indexOf('.')
|
||||
if (dotIndex !== -1) {
|
||||
field = key.substr(0, dotIndex)
|
||||
}
|
||||
changedFields.push(field)
|
||||
getChangedFields(fileId).push(field)
|
||||
|
||||
const value = newData[key]
|
||||
if (typeof value === 'undefined') {
|
||||
remove(data, key)
|
||||
remove(data[fileId], key)
|
||||
} else {
|
||||
set(data, key, value)
|
||||
set(data[fileId], key, value)
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -204,6 +240,7 @@ async function save (id, context) {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
writeData({ config, data, changedFields }, context)
|
||||
current = {}
|
||||
}
|
||||
@@ -222,7 +259,7 @@ function cancel (id, context) {
|
||||
module.exports = {
|
||||
list,
|
||||
findOne,
|
||||
getPrompts,
|
||||
getPromptTabs,
|
||||
save,
|
||||
cancel
|
||||
}
|
||||
|
||||
@@ -107,6 +107,7 @@ function generatePrompt (data) {
|
||||
value: null,
|
||||
valueChanged: false,
|
||||
error: null,
|
||||
tabId: data.tabId || null,
|
||||
raw: data
|
||||
}
|
||||
}
|
||||
@@ -171,7 +172,9 @@ function list () {
|
||||
}
|
||||
|
||||
function add (data) {
|
||||
prompts.push(generatePrompt(data))
|
||||
const prompt = generatePrompt(data)
|
||||
prompts.push(prompt)
|
||||
return prompt
|
||||
}
|
||||
|
||||
async function start () {
|
||||
|
||||
@@ -20,14 +20,21 @@ type Configuration implements DescribedEntity {
|
||||
description: String
|
||||
link: String
|
||||
icon: String
|
||||
prompts: [Prompt]
|
||||
plugin: Plugin
|
||||
tabs: [ConfigurationTab]!
|
||||
}
|
||||
|
||||
type ConfigurationTab {
|
||||
id: ID!
|
||||
label: String!
|
||||
icon: String
|
||||
prompts: [Prompt]
|
||||
}
|
||||
`
|
||||
|
||||
exports.resolvers = {
|
||||
Configuration: {
|
||||
prompts: (configuration, args, context) => configurations.getPrompts(configuration.id, context),
|
||||
tabs: (configuration, args, context) => configurations.getPromptTabs(configuration.id, context),
|
||||
plugin: (configuration, args, context) => plugins.findOne(configuration.pluginId, context)
|
||||
},
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ type Prompt implements DescribedEntity {
|
||||
value: String
|
||||
valueChanged: Boolean
|
||||
error: PromptError
|
||||
tabId: String
|
||||
}
|
||||
|
||||
input PromptInput {
|
||||
|
||||
@@ -5,8 +5,13 @@ query configuration ($id: ID!) {
|
||||
configuration(id: $id) {
|
||||
...configuration
|
||||
link
|
||||
prompts {
|
||||
...prompt
|
||||
tabs {
|
||||
id
|
||||
label
|
||||
icon
|
||||
prompts {
|
||||
...prompt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,4 +19,5 @@ fragment prompt on Prompt {
|
||||
error {
|
||||
...promptError
|
||||
}
|
||||
tabId
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ import PROMPT_ANSWER from '../graphql/promptAnswer.gql'
|
||||
|
||||
export default function ({
|
||||
field,
|
||||
query
|
||||
query,
|
||||
variables = null,
|
||||
updateQuery = null
|
||||
}) {
|
||||
// @vue/component
|
||||
return {
|
||||
@@ -32,6 +34,15 @@ export default function ({
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
hasPromptsChanged: {
|
||||
handler (value) {
|
||||
this.$emit('has-changes', value)
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async answerPrompt ({ prompt, value }) {
|
||||
await this.$apollo.mutate({
|
||||
@@ -43,13 +54,17 @@ export default function ({
|
||||
}
|
||||
},
|
||||
update: (store, { data: { promptAnswer } }) => {
|
||||
let variables = this.$apollo.queries[field].options.variables || undefined
|
||||
if (typeof variables === 'function') {
|
||||
variables = variables.call(this)
|
||||
let vars = variables || this.$apollo.queries[field].options.variables || undefined
|
||||
if (typeof vars === 'function') {
|
||||
vars = vars.call(this)
|
||||
}
|
||||
const data = store.readQuery({ query, variables })
|
||||
data[field].prompts = promptAnswer
|
||||
store.writeQuery({ query, variables, data })
|
||||
const data = store.readQuery({ query, variables: vars })
|
||||
if (updateQuery) {
|
||||
updateQuery.call(this, data, promptAnswer)
|
||||
} else {
|
||||
data[field].prompts = promptAnswer
|
||||
}
|
||||
store.writeQuery({ query, variables: vars, data })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,11 +1,32 @@
|
||||
<template>
|
||||
<div class="project-configuration-details">
|
||||
<div class="content">
|
||||
<PromptsList
|
||||
:prompts="visiblePrompts"
|
||||
@answer="answerPrompt"
|
||||
/>
|
||||
</div>
|
||||
<template v-if="configuration">
|
||||
<div v-if="configuration.tabs.length > 1" class="tabs">
|
||||
<VueGroup
|
||||
v-model="currentTab"
|
||||
class="tabs-selector"
|
||||
>
|
||||
<VueGroupButton
|
||||
v-for="tab of configuration.tabs"
|
||||
:key="tab.id"
|
||||
:value="tab.id"
|
||||
:icon-left="tab.icon"
|
||||
:label="$t(tab.label)"
|
||||
/>
|
||||
</VueGroup>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<ConfigurationTab
|
||||
v-for="tab of configuration.tabs"
|
||||
v-show="tab.id === currentTab"
|
||||
:key="tab.id"
|
||||
:configuration="configuration"
|
||||
:tab="tab"
|
||||
@has-changes="value => tabsHaveChanges[tab.id] = value"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="actions-bar space-between">
|
||||
<VueButton
|
||||
@@ -26,7 +47,15 @@
|
||||
/>
|
||||
|
||||
<VueButton
|
||||
:disabled="!hasPromptsChanged"
|
||||
v-if="configuration && !hasPromptsChanged"
|
||||
icon-left="refresh"
|
||||
class="big primary"
|
||||
:label="$t('views.project-configuration-details.actions.refresh')"
|
||||
@click="refetch()"
|
||||
/>
|
||||
|
||||
<VueButton
|
||||
v-else
|
||||
icon-left="save"
|
||||
class="primary big"
|
||||
:label="$t('views.project-configuration-details.actions.save')"
|
||||
@@ -37,20 +66,11 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Prompts from '../mixins/Prompts'
|
||||
|
||||
import CONFIGURATION from '../graphql/configuration.gql'
|
||||
import CONFIGURATION_SAVE from '../graphql/configurationSave.gql'
|
||||
import CONFIGURATION_CANCEL from '../graphql/configurationCancel.gql'
|
||||
|
||||
export default {
|
||||
mixins: [
|
||||
Prompts({
|
||||
field: 'configuration',
|
||||
query: CONFIGURATION
|
||||
})
|
||||
],
|
||||
|
||||
metaInfo () {
|
||||
return {
|
||||
title: this.configuration && `${this.configuration.name} - ${this.$t('views.project-configurations.title')}`
|
||||
@@ -66,7 +86,9 @@ export default {
|
||||
|
||||
data () {
|
||||
return {
|
||||
configuration: null
|
||||
configuration: null,
|
||||
currentTab: '__default',
|
||||
tabsHaveChanges: {}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -77,11 +99,44 @@ export default {
|
||||
return {
|
||||
id: this.id
|
||||
}
|
||||
},
|
||||
async result ({ data, loading }) {
|
||||
if (!this.$_init && !loading && data && data.configuration) {
|
||||
this.$_init = true
|
||||
this.tabsHaveChanges = data.configuration.tabs.reduce((obj, tab) => {
|
||||
obj[tab.id] = false
|
||||
return obj
|
||||
}, {})
|
||||
await this.$nextTick()
|
||||
this.currentTab = data.configuration.tabs[0].id
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
hasPromptsChanged () {
|
||||
for (const key in this.tabsHaveChanges) {
|
||||
if (this.tabsHaveChanges[key]) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
id: 'init'
|
||||
},
|
||||
|
||||
created () {
|
||||
this.init()
|
||||
},
|
||||
|
||||
methods: {
|
||||
init (tab) {
|
||||
this.currentTab = '__default'
|
||||
this.$_init = false
|
||||
},
|
||||
|
||||
async cancel () {
|
||||
await this.$apollo.mutate({
|
||||
mutation: CONFIGURATION_CANCEL,
|
||||
@@ -90,7 +145,7 @@ export default {
|
||||
}
|
||||
})
|
||||
|
||||
this.$apollo.queries.configuration.refetch()
|
||||
this.refetch()
|
||||
},
|
||||
|
||||
async save () {
|
||||
@@ -101,6 +156,10 @@ export default {
|
||||
}
|
||||
})
|
||||
|
||||
this.refetch()
|
||||
},
|
||||
|
||||
refetch () {
|
||||
this.$apollo.queries.configuration.refetch()
|
||||
}
|
||||
}
|
||||
@@ -120,4 +179,7 @@ export default {
|
||||
height 0
|
||||
overflow-x hidden
|
||||
overflow-y auto
|
||||
|
||||
.tabs
|
||||
margin $padding-item 0
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
describe('Configurations', () => {
|
||||
it('Displays configurations', () => {
|
||||
cy.visit('/configuration')
|
||||
cy.get('.configuration-item').should('have.length', 3)
|
||||
cy.get('.configuration-item').should('have.length', 2)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user