feat: allow saving multiple presets

This commit is contained in:
Evan You
2018-01-28 00:24:33 -05:00
parent 9856549432
commit f372f55e93
19 changed files with 252 additions and 193 deletions
+2
View File
@@ -28,6 +28,8 @@ exports.prompt = prompts => {
const a = pendingAssertions[i - skipped]
if (!a) {
console.error(`no matching assertion for prompt:`, prompt)
console.log(prompts)
console.log(pendingAssertions)
}
if (a.message) {
@@ -3,15 +3,15 @@ jest.setTimeout(30000)
const path = require('path')
const portfinder = require('portfinder')
const { createServer } = require('http-server')
const { defaults } = require('@vue/cli/lib/options')
const { defaultPreset } = require('@vue/cli/lib/options')
const create = require('@vue/cli-test-utils/createTestProject')
const launchPuppeteer = require('@vue/cli-test-utils/launchPuppeteer')
let server, browser
test('pwa', async () => {
// it's ok to mutate here since jest loads each test in a separate vm
defaults.plugins['@vue/cli-plugin-pwa'] = {}
const project = await create('pwa-build', defaults)
defaultPreset.plugins['@vue/cli-plugin-pwa'] = {}
const project = await create('pwa-build', defaultPreset)
expect(project.has('src/registerServiceWorker.js')).toBe(true)
const { stdout } = await project.run('vue-cli-service build')
@@ -3,13 +3,13 @@ jest.setTimeout(30000)
const path = require('path')
const portfinder = require('portfinder')
const { createServer } = require('http-server')
const { defaults } = require('@vue/cli/lib/options')
const { defaultPreset } = require('@vue/cli/lib/options')
const create = require('@vue/cli-test-utils/createTestProject')
const launchPuppeteer = require('@vue/cli-test-utils/launchPuppeteer')
let server, browser, page
test('build', async () => {
const project = await create('e2e-build', defaults)
const project = await create('e2e-build', defaultPreset)
// test public copy
project.write('public/foo.js', '1')
@@ -3,13 +3,13 @@ jest.setTimeout(30000)
const path = require('path')
const portfinder = require('portfinder')
const { createServer } = require('http-server')
const { defaults } = require('@vue/cli/lib/options')
const { defaultPreset } = require('@vue/cli/lib/options')
const create = require('@vue/cli-test-utils/createTestProject')
const launchPuppeteer = require('@vue/cli-test-utils/launchPuppeteer')
let server, browser, page
test('build with DLL', async () => {
const project = await create('e2e-build-dll', Object.assign({}, defaults, {
const project = await create('e2e-build-dll', Object.assign({}, defaultPreset, {
router: true,
vuex: true
}))
@@ -1,13 +1,13 @@
jest.setTimeout(30000)
const { defaults } = require('@vue/cli/lib/options')
const { defaultPreset } = require('@vue/cli/lib/options')
const create = require('@vue/cli-test-utils/createTestProject')
const serve = require('@vue/cli-test-utils/serveWithPuppeteer')
const sleep = n => new Promise(resolve => setTimeout(resolve, n))
test('serve', async () => {
const project = await create('e2e-serve', defaults)
const project = await create('e2e-serve', defaultPreset)
await serve(
() => project.run('vue-cli-service serve'),
@@ -26,7 +26,7 @@ test('serve', async () => {
})
test('serve with router', async () => {
const project = await create('e2e-serve-router', Object.assign({}, defaults, {
const project = await create('e2e-serve-router', Object.assign({}, defaultPreset, {
router: true
}))
@@ -1,4 +1,4 @@
module.exports = function silence (exports) {
module.exports = function silence (logName, exports) {
const logs = {}
Object.keys(exports).forEach(key => {
if (key !== 'error') {
@@ -8,5 +8,5 @@ module.exports = function silence (exports) {
}
}
})
exports.logs = logs
exports[logName] = logs
}
+1 -1
View File
@@ -45,5 +45,5 @@ exports.clearConsole = title => {
// silent all logs except errors during tests and keep record
if (process.env.VUE_CLI_TEST) {
require('./_silence')(exports)
require('./_silence')('logs', exports)
}
@@ -45,5 +45,5 @@ exports.resumeSpinner = () => {
// silent all logs except errors during tests and keep record
if (process.env.VUE_CLI_TEST) {
require('./_silence')(exports)
require('./_silence')('spinner', exports)
}
@@ -4,11 +4,15 @@ const { error } = require('./logger')
// proxy to joi for option validation
exports.createSchema = fn => fn(joi)
exports.validate = (obj, schema, options = {}) => {
exports.validate = (obj, schema, options = {}, noExit) => {
joi.validate(obj, schema, options, err => {
if (err) {
error(`vue-cli options validation failed:\n` + err.message)
process.exit(1)
if (!noExit) {
process.exit(1)
} else {
throw err
}
}
})
}
@@ -1,6 +1,7 @@
// using this requires mocking fs & inquirer
const Creator = require('@vue/cli/lib/Creator')
const { loadOptions } = require('@vue/cli/lib/options')
const { expectPrompts } = require('inquirer') // from mock
module.exports = async function assertPromptModule (
@@ -13,7 +14,7 @@ module.exports = async function assertPromptModule (
if (opts.plguinsOnly) {
expectedPrompts.unshift(
{
message: 'project creation mode',
message: 'Please pick a preset',
choose: 1
}
)
@@ -23,23 +24,24 @@ module.exports = async function assertPromptModule (
choose: 1 // package.json
},
{
message: 'package manager',
choose: 0 // yarn
},
{
message: 'Save the preferences',
message: 'Save this as a preset',
confirm: false
}
)
if (!loadOptions().packageManager) {
expectedPrompts.push({
message: 'package manager',
choose: 0 // yarn
})
}
}
expectPrompts(expectedPrompts)
const creator = new Creator('test', '/', [].concat(module))
const options = await creator.promptAndResolveOptions()
const preset = await creator.promptAndResolvePreset()
if (opts.plguinsOnly) {
delete options.packageManager
delete options.useConfigFiles
delete preset.useConfigFiles
}
expect(options).toEqual(expectedOptions)
expect(preset).toEqual(expectedOptions)
}
@@ -6,15 +6,9 @@ const readFile = promisify(fs.readFile)
const writeFile = promisify(fs.writeFile)
const mkdirp = promisify(require('mkdirp'))
module.exports = function createTestProject (name, config, cwd) {
module.exports = function createTestProject (name, preset, cwd) {
cwd = cwd || path.resolve(__dirname, '../../test')
config = Object.assign({
packageManager: 'yarn',
useTaobaoRegistry: false,
plugins: {}
}, config)
const projectRoot = path.resolve(cwd, name)
const read = file => {
@@ -46,8 +40,8 @@ module.exports = function createTestProject (name, config, cwd) {
'create',
name,
'--force',
'--config',
JSON.stringify(config)
'--inlinePreset',
JSON.stringify(preset)
]
const options = {
+14 -11
View File
@@ -7,15 +7,20 @@ const assertPromptModule = require('@vue/cli-test-utils/assertPromptModule')
test('default', async () => {
const epxectedPrompts = [
{
message: 'project creation mode',
message: 'pick a preset',
choices: [
'Zero-config',
'default',
'Manually select'
],
choose: 0
},
{
message: 'package manager',
choices: ['Yarn', 'NPM'],
choose: 0
}
]
assertPromptModule([], epxectedPrompts, defaults)
await assertPromptModule([], epxectedPrompts, defaults.presets.default)
})
test('manual + PromptModuleAPI', async () => {
@@ -73,18 +78,16 @@ test('manual + PromptModuleAPI', async () => {
choose: 0
},
{
message: 'package manager',
choices: ['Yarn', 'NPM'],
choose: 0
message: 'Save this as a preset',
confirm: true
},
{
message: 'Save the preferences',
confirm: true
message: 'Save preset as',
input: 'test'
}
]
const expectedOptions = {
packageManager: 'yarn',
useConfigFiles: true,
plugins: {
bar: {},
@@ -98,8 +101,8 @@ test('manual + PromptModuleAPI', async () => {
const expectedPromptsForSaved = [
{
choices: [
'Use previously saved',
'Zero-config',
'test',
'default',
'Manually'
],
choose: 0
@@ -323,10 +323,7 @@ test('extract config files', async () => {
},
jest: {
foo: 'bar'
},
browserslist: [
'>=ie9'
]
}
}
const generator = new Generator('/', {}, [
@@ -345,6 +342,5 @@ test('extract config files', async () => {
expect(fs.readFileSync('/.babelrc', 'utf-8')).toMatch(json(configs.babel))
expect(fs.readFileSync('/.postcssrc', 'utf-8')).toMatch(json(configs.postcss))
expect(fs.readFileSync('/.eslintrc', 'utf-8')).toMatch(json(configs.eslintConfig))
expect(fs.readFileSync('/.browserslistrc', 'utf-8')).toMatch(configs.browserslist.join('\n'))
expect(fs.readFileSync('/jest.config.js', 'utf-8')).toMatch(`module.exports = {\n foo: 'bar'\n}`)
})
+28 -24
View File
@@ -4,16 +4,17 @@ const fs = require('fs')
const {
rcPath,
loadOptions,
saveOptions
saveOptions,
savePreset
} = require('../lib/options')
test('load options', () => {
expect(loadOptions()).toEqual({})
fs.writeFileSync(rcPath, JSON.stringify({
plugins: {}
presets: {}
}, null, 2))
expect(loadOptions()).toEqual({
plugins: {}
presets: {}
})
})
@@ -22,49 +23,52 @@ test('should not save unknown fields', () => {
foo: 'bar'
})
expect(loadOptions()).toEqual({
plugins: {}
presets: {}
})
})
test('save options (merge)', () => {
test('save options', () => {
// partial
saveOptions({
packageManager: 'yarn'
})
expect(loadOptions()).toEqual({
packageManager: 'yarn',
plugins: {}
presets: {}
})
// replace
saveOptions({
plugins: {
presets: {
foo: { a: 1 }
}
})
expect(loadOptions()).toEqual({
packageManager: 'yarn',
plugins: {
presets: {
foo: { a: 1 }
}
})
})
// shallow save should replace fields
saveOptions({
plugins: {
bar: { b: 2 }
}
})
test('save preset', () => {
savePreset('bar', { a: 2 })
expect(loadOptions()).toEqual({
packageManager: 'yarn',
plugins: {
bar: { b: 2 }
presets: {
foo: { a: 1 },
bar: { a: 2 }
}
})
// should entirely replace presets
savePreset('foo', { c: 3 })
savePreset('bar', { d: 4 })
expect(loadOptions()).toEqual({
packageManager: 'yarn',
presets: {
foo: { c: 3 },
bar: { d: 4 }
}
})
})
test('save options (replace)', () => {
const toSave = {
foo: 'bar'
}
saveOptions(toSave, true)
expect(loadOptions()).toEqual(toSave)
})
+2 -2
View File
@@ -37,9 +37,9 @@ program
program
.command('create <app-name>')
.description('create a new project powered by vue-cli-service')
.option('-s, --saved', 'Skip prompts and use saved config')
.option('-p, --preset <presetName>', 'Skip prompts and use saved preset')
.option('-d, --default', 'Skip prompts and use default config')
.option('-c, --config <json>', 'Skip prompts and use inline JSON string as config')
.option('-i, --inlinePreset <json>', 'Skip prompts and use inline JSON string as preset')
.option('-r, --registry <url>', 'Use specified NPM registry when installing dependencies')
.option('-m, --packageManager <command>', 'Use specified NPM client when installing dependencies')
.option('-f, --force', 'Overwrite target directory if it exists')
+113 -80
View File
@@ -16,9 +16,10 @@ const setupDevProject = require('./util/setupDevProject')
const {
defaults,
validate,
saveOptions,
loadOptions
loadOptions,
savePreset,
validatePreset
} = require('./options')
const {
@@ -30,14 +31,14 @@ const {
stopSpinner
} = require('@vue/cli-shared-utils')
const isMode = _mode => ({ mode }) => _mode === mode
const isManualMode = answers => answers.preset === '__manual__'
module.exports = class Creator {
constructor (name, context, promptModules) {
this.name = name
this.context = process.env.VUE_CLI_CONTEXT = context
const { modePrompt, featurePrompt } = this.resolveIntroPrompts()
this.modePrompt = modePrompt
const { presetPrompt, featurePrompt } = this.resolveIntroPrompts()
this.presetPrompt = presetPrompt
this.featurePrompt = featurePrompt
this.outroPrompts = this.resolveOutroPrompts()
this.injectedPrompts = []
@@ -56,32 +57,35 @@ module.exports = class Creator {
return execa(command, args, { cwd: context })
}
let options
if (cliOptions.saved) {
options = loadOptions()
let preset
if (cliOptions.preset) {
// vue create foo --preset bar
preset = this.resolvePreset(cliOptions.preset)
} else if (cliOptions.default) {
options = defaults
} else if (cliOptions.config) {
// vue create foo --default
preset = defaults.presets.default
} else if (cliOptions.inlinePreset) {
// vue create foo --inlinePreset {...}
try {
options = JSON.parse(cliOptions.config)
preset = JSON.parse(cliOptions.inlinePreset)
} catch (e) {
error(`CLI inline config is not valid JSON: ${cliOptions.config}`)
error(`CLI inline preset is not valid JSON: ${cliOptions.inlinePreset}`)
process.exit(1)
}
} else {
options = await this.promptAndResolveOptions()
preset = await this.promptAndResolvePreset()
}
// clone before mutating
options = cloneDeep(options)
preset = cloneDeep(preset)
// inject core service
options.plugins['@vue/cli-service'] = Object.assign({
preset.plugins['@vue/cli-service'] = Object.assign({
projectName: name
}, options)
}, preset)
const packageManager = (
cliOptions.packageManager ||
options.packageManager ||
loadOptions().packageManager ||
(hasYarn ? 'yarn' : 'npm')
)
@@ -103,7 +107,7 @@ module.exports = class Creator {
private: true,
devDependencies: {}
}
const deps = Object.keys(options.plugins)
const deps = Object.keys(preset.plugins)
deps.forEach(dep => {
pkg.devDependencies[dep] = `^${latestCLIVersion}`
})
@@ -133,12 +137,12 @@ module.exports = class Creator {
// run generator
log()
log(`🚀 Invoking generators...`)
const plugins = this.resolvePlugins(options.plugins)
const plugins = this.resolvePlugins(preset.plugins)
const generator = new Generator(
context,
pkg,
plugins,
options.useConfigFiles,
preset.useConfigFiles,
createCompleteCbs
)
await generator.generate()
@@ -174,43 +178,69 @@ module.exports = class Creator {
log(
`👉 Get started with the following commands:\n\n` +
chalk.cyan(` ${chalk.gray('$')} cd ${name}\n`) +
chalk.cyan(` ${chalk.gray('$')} ${options.packageManager === 'yarn' ? 'yarn serve' : 'npm run serve'}`)
chalk.cyan(` ${chalk.gray('$')} ${packageManager === 'yarn' ? 'yarn serve' : 'npm run serve'}`)
)
log()
}
async promptAndResolveOptions () {
async promptAndResolvePreset () {
// prompt
clearConsole()
const answers = await inquirer.prompt(this.resolveFinalPrompts())
debug('vue:cli-answers')(answers)
debug('vue-cli:answers')(answers)
let options
if (answers.mode === 'saved') {
options = loadOptions()
} else if (answers.mode === 'default') {
options = defaults
if (answers.packageManager) {
saveOptions({
packageManager: answers.packageManager
})
}
let preset
if (answers.preset && answers.preset !== '__manual__') {
preset = this.resolvePreset(answers.preset)
} else {
// manual
options = {
packageManager: answers.packageManager || loadOptions().packageManager,
preset = {
useConfigFiles: answers.useConfigFiles === 'files',
plugins: {}
}
// run cb registered by prompt modules to finalize the options
this.promptCompleteCbs.forEach(cb => cb(answers, options))
answers.features = answers.features || []
// run cb registered by prompt modules to finalize the preset
this.promptCompleteCbs.forEach(cb => cb(answers, preset))
}
// validate
validate(options)
validatePreset(preset)
// save options
if (answers.mode === 'manual' && answers.save) {
saveOptions(options, true /* replace */)
// save preset
if (answers.save && answers.saveName) {
savePreset(answers.saveName, preset)
}
debug('vue:cli-options')(options)
return options
debug('vue-cli:preset')(preset)
return preset
}
resolvePreset (name) {
const savedPresets = loadOptions().presets || {}
let preset = savedPresets[name]
// use default preset if user has not overwritten it
if (name === 'default' && !preset) {
preset = defaults.presets.default
}
if (!preset) {
error(`preset "${name}" not found.`)
const presets = Object.keys(savedPresets)
if (presets.length) {
log()
log(`available presets:\n${presets.join(`\n`)}`)
} else {
log(`you don't seem to have any saved preset.`)
log(`run vue-cli in manual mode to create a preset.`)
}
process.exit(1)
}
return preset
}
// { id: options } => [{ id, apply, options }]
@@ -228,51 +258,45 @@ module.exports = class Creator {
}
resolveIntroPrompts () {
const defualtFeatures = formatFeatures(defaults)
const modePrompt = {
name: 'mode',
const savedOptions = loadOptions()
const presets = Object.assign({}, savedOptions.presets, defaults.presets)
const presetChoices = Object.keys(presets).map(name => {
return {
name: `${name} (${formatFeatures(presets[name])})`,
value: name
}
})
const presetPrompt = {
name: 'preset',
type: 'list',
message: `Please pick a project creation mode:`,
message: `Please pick a preset:`,
choices: [
{
name: `Zero-config with defaults (${defualtFeatures})`,
value: 'default'
},
...presetChoices,
{
name: 'Manually select features',
value: 'manual'
value: '__manual__'
}
]
}
const savedOptions = loadOptions()
if (savedOptions.plugins) {
const savedFeatures = formatFeatures(savedOptions)
modePrompt.choices.unshift({
name: `Use previously saved config (${savedFeatures})`,
value: 'saved'
})
}
const featurePrompt = {
name: 'features',
when: isMode('manual'),
when: isManualMode,
type: 'checkbox',
message: 'Check the features needed for your project:',
choices: [],
pageSize: 8
}
return {
modePrompt,
presetPrompt,
featurePrompt
}
}
resolveOutroPrompts () {
const outroPrompts = []
const savedOptions = loadOptions()
if (savedOptions.useConfigFiles == null) {
outroPrompts.push({
const outroPrompts = [
{
name: 'useConfigFiles',
when: isMode('manual'),
when: isManualMode,
type: 'list',
message: 'Where do you prefer placing config for Babel, PostCSS, ESLint, etc.?',
choices: [
@@ -285,12 +309,26 @@ module.exports = class Creator {
value: 'pkg'
}
]
})
}
},
{
name: 'save',
when: isManualMode,
type: 'confirm',
message: 'Save this as a preset for future projects?'
},
{
name: 'saveName',
when: answers => answers.save,
type: 'input',
message: 'Save preset as:'
}
]
// ask for packageManager once
const savedOptions = loadOptions()
if (hasYarn && !savedOptions.packageManager) {
outroPrompts.push({
name: 'packageManager',
when: isMode('manual'),
type: 'list',
message: 'Pick the package manager to use when installing dependencies:',
choices: [
@@ -307,30 +345,25 @@ module.exports = class Creator {
]
})
}
outroPrompts.push({
name: 'save',
when: isMode('manual'),
type: 'confirm',
message: 'Save the preferences for future projects? (You can always manually edit ~/.vuerc)'
})
return outroPrompts
}
resolveFinalPrompts () {
// patch generator-injected prompts to only show when mode === 'manual'
// patch generator-injected prompts to only show in manual mode
this.injectedPrompts.forEach(prompt => {
const originalWhen = prompt.when || (() => true)
prompt.when = options => {
return options.mode === 'manual' && originalWhen(options)
prompt.when = answers => {
return isManualMode(answers) && originalWhen(answers)
}
})
const prompts = [].concat(
this.modePrompt,
const prompts = [
this.presetPrompt,
this.featurePrompt,
this.injectedPrompts,
this.outroPrompts
)
debug('vue:cli-prompts')(prompts)
...this.injectedPrompts,
...this.outroPrompts
]
debug('vue-cli:prompts')(prompts)
return prompts
}
}
+43 -28
View File
@@ -1,44 +1,49 @@
const fs = require('fs')
const os = require('os')
const path = require('path')
const {
error,
hasYarn,
createSchema,
validate
} = require('@vue/cli-shared-utils')
const cloneDeep = require('lodash.clonedeep')
const { error, log, createSchema, validate } = require('@vue/cli-shared-utils')
const rcPath = exports.rcPath = (
process.env.VUE_CLI_CONFIG_PATH ||
path.join(os.homedir(), '.vuerc')
)
const schema = createSchema(joi => joi.object().keys({
const presetSchema = createSchema(joi => joi.object().keys({
useConfigFiles: joi.boolean(),
router: joi.boolean(),
vuex: joi.boolean(),
cssPreprocessor: joi.string().only(['sass', 'less', 'stylus']),
useTaobaoRegistry: joi.boolean(),
packageManager: joi.string().only(['yarn', 'npm']),
useConfigFiles: joi.boolean(),
plugins: joi.object().required()
}))
exports.validate = options => validate(options, schema)
const schema = createSchema(joi => joi.object().keys({
packageManager: joi.string().only(['yarn', 'npm']),
useTaobaoRegistry: joi.boolean(),
presets: joi.object().pattern(/^/, presetSchema)
}))
exports.defaults = {
exports.validatePreset = preset => validate(preset, presetSchema)
exports.defaultPreset = {
router: false,
vuex: false,
useConfigFiles: false,
cssPreprocessor: undefined,
useConfigFiles: undefined,
useTaobaoRegistry: undefined,
packageManager: hasYarn ? 'yarn' : 'npm',
plugins: {
'@vue/cli-plugin-babel': {},
'@vue/cli-plugin-eslint': {
config: 'base',
lintOn: ['save', 'commit']
},
'@vue/cli-plugin-unit-mocha': {}
lintOn: ['save']
}
}
}
exports.defaults = {
packageManager: undefined,
useTaobaoRegistry: undefined,
presets: {
'default': exports.defaultPreset
}
}
@@ -51,28 +56,32 @@ exports.loadOptions = () => {
if (fs.existsSync(rcPath)) {
try {
cachedOptions = JSON.parse(fs.readFileSync(rcPath, 'utf-8'))
return cachedOptions
} catch (e) {
error(
`Error loading saved preferences: ` +
`~/.vuerc may be corrupted or have syntax errors. ` +
`You may need to delete it and re-run vue-cli in manual mode.\n` +
`Please fix/delete it and re-run vue-cli in manual mode.\n` +
`(${e.message})`,
)
process.exit(1)
// process.exit(1)
}
try {
validate(cachedOptions, schema, undefined, true /* noExit */)
} catch (e) {
log(
`~/.vuerc may be outdated. ` +
`Please delete it and re-run vue-cli in manual mode.`
)
// process.exit(1)
}
return cachedOptions
} else {
return {}
}
}
exports.saveOptions = (toSave, replace) => {
let options
if (replace) {
options = toSave
} else {
options = Object.assign(exports.loadOptions(), toSave)
}
exports.saveOptions = toSave => {
const options = Object.assign(cloneDeep(exports.loadOptions()), toSave)
for (const key in options) {
if (!(key in exports.defaults)) {
delete options[key]
@@ -89,3 +98,9 @@ exports.saveOptions = (toSave, replace) => {
)
}
}
exports.savePreset = (name, preset) => {
const presets = cloneDeep(exports.loadOptions().presets || {})
presets[name] = preset
exports.saveOptions({ presets })
}
+7 -4
View File
@@ -22,11 +22,14 @@ module.exports = {
jest: {
filename: 'jest.config.js',
transform: js
},
browserslist: {
filename: '.browserslistrc',
transform: value => value.join('\n')
}
// these are less likely to be edited frequently
// browserslist: {
// filename: '.browserslistrc',
// transform: value => value.join('\n')
// },
// 'lint-staged': {
// filename: '.lintstagedrc',
// transform: json
+7 -4
View File
@@ -1,14 +1,17 @@
const chalk = require('chalk')
module.exports = function formatFeatures (options, lead, joiner) {
module.exports = function formatFeatures (preset, lead, joiner) {
const features = []
if (options.router) {
if (preset.router) {
features.push('vue-router')
}
if (options.vuex) {
if (preset.vuex) {
features.push('vuex')
}
const plugins = Object.keys(options.plugins).filter(dep => {
if (preset.cssPreprocessor) {
features.push(preset.cssPreprocessor)
}
const plugins = Object.keys(preset.plugins).filter(dep => {
return dep !== '@vue/cli-service'
})
features.push.apply(features, plugins)