refactor: adjust generation strategy

This commit is contained in:
Evan You
2017-12-28 22:10:32 -05:00
parent 4cbd95da19
commit 79cfab8edb
41 changed files with 336 additions and 260 deletions

View File

@@ -0,0 +1,10 @@
module.exports = api => {
api.extendPackage({
devDependencies: {
'babel-preset-vue-app': '^2.0.0'
},
babel: {
presets: ['vue-app'] // TODO update babel-preset-vue-app
}
})
}

View File

@@ -0,0 +1,3 @@
module.exports = (api, options) => {
}

View File

@@ -0,0 +1,3 @@
module.exports = (api, options) => {
}

View File

@@ -0,0 +1,12 @@
import { shallow } from 'vue-test-utils'
import HelloWorld from '@/components/HelloWorld.vue'
describe('Hello.vue', () => {
it('renders props.msg when passed', () => {
const msg = 'new message'
const wrapper = shallow(HelloWorld, {
context: { props: { msg } }
})
expect(wrapper.text()).toBe(msg)
})
})

View File

@@ -5,7 +5,6 @@ module.exports = (api, options) => {
test: 'jest'
},
devDependencies: {
'@vue/cli-plugin-unit-jest': '^0.1.0',
'jest': '^22.0.4',
'vue-test-utils': '^1.0.0-beta.9'
},

View File

@@ -1,5 +1,5 @@
import { shallow } from 'vue-test-utils'
<%_ if (options.assertionLibrary === 'expect' && options.unit !== 'jest') { _%>
<%_ if (options.assertionLibrary === 'expect') { _%>
import { expect } from 'expect'
<%_ } _%>
<%_ if (options.assertionLibrary === 'chai') { _%>
@@ -13,7 +13,7 @@ describe('Hello.vue', () => {
const wrapper = shallow(HelloWorld, {
context: { props: { msg } }
})
<%_ if (options.assertionLibrary === 'expect' || options.unit === 'jest') { _%>
<%_ if (options.assertionLibrary === 'expect') { _%>
expect(wrapper.text()).toBe(msg)
<%_ } else if (options.assertionLibrary === 'chai') { _%>
expect(wrapper.text()).to.equal(msg)

View File

@@ -1,6 +1,7 @@
module.exports = (api, options) => {
api.renderFiles('./files')
const devDependencies = {
'@vue/cli-plugin-unit-mocha-webpack': '^0.1.0',
'vue-test-utils': '^1.0.0-beta.9'
}
if (options.assertionLibrary === 'chai') {
@@ -15,6 +16,4 @@ module.exports = (api, options) => {
test: 'vue-cli-service test'
}
})
api.renderFiles('./files')
}

View File

@@ -2,12 +2,12 @@
const semver = require('semver')
const { error } = require('@vue/cli-shared-utils')
const requiredVersion = require('../package.json').engine.node
const requiredVersion = require('../package.json').engines.node
if (!semver.satisfies(process.version, requiredVersion)) {
error(
`You are using Node ${process.version}, but vue-cli-service\n` +
`requires Node ${requiredVersion}. Please upgrade your Node version.`
`You are using Node ${process.version}, but vue-cli-service ` +
`requires Node ${requiredVersion}.\nPlease upgrade your Node version.`
)
process.exit(1)
}

View File

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -0,0 +1,32 @@
module.exports = (generatorAPI, options) => {
generatorAPI.renderFiles('./files')
generatorAPI.extendPackage({
scripts: {
'dev': 'vue-cli-service serve' + (
// only auto open browser on MacOS where applescript
// can avoid dupilcate window opens
process.platform === 'darwin'
? ' --open'
: ''
),
'build': 'vue-cli-service build',
'start': 'vue-cli-service serve --prod'
},
dependencies: {
'vue': '^2.5.13'
},
devDependencies: {
'vue-template-compiler': '^2.5.13'
},
'postcss': {
'plugins': {
'autoprefixer': {}
}
},
browserslist: [
'> 1%',
'last 2 versions',
'not ie <= 8'
]
})
}

View File

@@ -2,12 +2,12 @@
const semver = require('semver')
const { error } = require('@vue/cli-shared-utils')
const requiredVersion = require('../package.json').engine.node
const requiredVersion = require('../package.json').engines.node
if (!semver.satisfies(process.version, requiredVersion)) {
error(
`You are using Node ${process.version}, but this version of vue-cli\n` +
`requires Node ${requiredVersion}. Please upgrade your Node version.`
`You are using Node ${process.version}, but this version of vue-cli ` +
`requires Node ${requiredVersion}.\nPlease upgrade your Node version.`
)
process.exit(1)
} else {

View File

@@ -1,106 +1,115 @@
const fs = require('fs')
const os = require('os')
const ejs = require('ejs')
const path = require('path')
const chalk = require('chalk')
const debug = require('debug')
const inquirer = require('inquirer')
const GeneratorAPI = require('./GeneratorAPI')
const Generator = require('./Generator')
const installDeps = require('./util/installDeps')
const PromptModuleAPI = require('./PromptModuleAPI')
const writeFileTree = require('./util/writeFileTree')
const {
info,
error,
success,
hasYarn,
clearConsole
} = require('@vue/cli-shared-utils')
const debug = require('debug')
const rcPath = path.join(os.homedir(), '.vuerc')
const isMode = _mode => ({ mode }) => _mode === mode
const defaultOptions = {
features: ['eslint', 'unit'],
eslint: 'eslint-only',
unit: 'mocha',
assertionLibrary: 'chai',
packageManager: hasYarn ? 'yarn' : 'npm'
packageManager: hasYarn ? 'yarn' : 'npm',
plugins: {
'@vue/cli-plugin-babel': {},
'@vue/cli-plugin-eslint': { config: 'eslint-only' },
'@vue/cli-plugin-unit-mocha-webpack': { assertionLibrary: 'chai' }
}
}
module.exports = class Creator {
constructor (name, generators) {
this.name = name
constructor (modules) {
const { modePrompt, featurePrompt } = this.resolveIntroPrompts()
this.modePrompt = modePrompt
this.featurePrompt = featurePrompt
this.outroPrompts = this.resolveOutroPrompts()
this.injectedPrompts = []
this.promptCompleteCbs = []
this.fileMiddlewares = []
this.options = {}
this.pkg = {
name,
version: '0.1.0',
private: true,
scripts: {},
dependencies: {},
devDependencies: {}
}
// for conflict resolution
this.depSources = {}
// virtual file tree
this.files = {}
generators.forEach(({ id, apply }) => {
apply(new GeneratorAPI(id, this))
})
const api = new PromptModuleAPI(this)
modules.forEach(m => m(api))
}
async create (targetDir) {
async create (name, targetDir) {
// prompt
clearConsole()
let options = await inquirer.prompt(this.resolveFinalPrompts())
debug('rawOptions')(options)
const answers = await inquirer.prompt(this.resolveFinalPrompts())
debug('answers')(answers)
if (options.mode === 'saved') {
let options
if (answers.mode === 'saved') {
options = this.loadSavedOptions()
} else if (options.mode === 'default') {
} else if (answers.mode === 'default') {
options = defaultOptions
} else {
options = {
packageManager: answers.packageManager,
plugins: {}
}
}
options.projectName = this.name
// run cb registered by generators
this.promptCompleteCbs.forEach(cb => cb(options))
this.options = options
debug('options')(options)
// run cb registered by prompt modules to finalize the options
this.promptCompleteCbs.forEach(cb => cb(answers, options))
// save options
if (options.mode === 'manual' && options.save) {
if (answers.mode === 'manual' && answers.save) {
this.saveOptions(options)
}
// wait for file resolve
await this.resolveFiles()
// set package.json
this.resolvePkg()
this.files['package.json'] = JSON.stringify(this.pkg, null, 2)
// write file tree to disk
await writeFileTree(targetDir, this.files)
success(`Project created in ${chalk.cyan(options.projectName)}.`)
if (options.packageManager) {
info(`Installing dependencies with ${options.packageManager}. This may take a while...`)
await installDeps(options.packageManager, targetDir)
// inject core service
options.plugins['@vue/cli-service'] = {
projectName: name
}
debug('options')(options)
// write base package.json to disk
info(`Creating project in ${chalk.cyan(targetDir)}.`)
writeFileTree(targetDir, {
'package.json': JSON.stringify({
name,
version: '0.1.0',
private: true
}, null, 2)
})
// install deps
info(`Installing dependencies with ${options.packageManager}. This may take a while...`)
const deps = Object.keys(options.plugins)
if (process.env.VUE_CLI_DEBUG) {
// in development, use linked packages
updatePackageForDev(targetDir, deps)
await installDeps(options.packageManager, targetDir)
} else {
await installDeps(options.packageManager, targetDir, deps)
}
// run generator
const generator = new Generator(targetDir, options)
await generator.generate()
// install deps again (new deps injected by generators)
await installDeps(options.packageManager, targetDir)
// TODO run vue-cli-service init
}
resolveIntroPrompts () {
const modePrompt = {
name: 'mode',
type: 'list',
message: `Pick a project creation mode:`,
message: `Hi there! Please pick a project creation mode:`,
choices: [
{
name: 'Zero-configuration with defaults',
@@ -122,7 +131,7 @@ module.exports = class Creator {
name: 'features',
when: isMode('manual'),
type: 'checkbox',
message: 'Please check the features needed for your project.',
message: 'Check the features needed for your project:',
choices: []
}
return {
@@ -132,46 +141,33 @@ module.exports = class Creator {
}
resolveOutroPrompts () {
const outroPrompts = [
{
name: 'save',
when: isMode('manual'),
type: 'confirm',
message: 'Save the preferences for future projects?'
}
]
const outroPrompts = []
if (hasYarn) {
outroPrompts.unshift({
outroPrompts.push({
name: 'packageManager',
when: isMode('manual'),
type: 'list',
message: 'Automatically install NPM dependencies after project creation?',
message: 'Pick the package manager to use when installing dependencies:',
choices: [
{
name: 'Use NPM',
value: 'npm',
short: 'NPM'
},
{
name: 'Use Yarn',
value: 'yarn',
short: 'Yarn'
},
{
name: `I'll handle that myself`,
value: false,
short: 'No'
name: 'Use NPM',
value: 'npm',
short: 'NPM'
}
]
})
} else {
outroPrompts.unshift({
name: 'packageManager',
when: isMode('manual'),
type: 'confirm',
message: 'Automatically install NPM dependencies after project creation?'
})
}
outroPrompts.push({
name: 'save',
when: isMode('manual'),
type: 'confirm',
message: 'Save the preferences for future projects?'
})
return outroPrompts
}
@@ -208,6 +204,10 @@ module.exports = class Creator {
}
saveOptions (options) {
options = Object.assign({}, options)
delete options.projectName
delete options.mode
delete options.save
try {
fs.writeFileSync(rcPath, JSON.stringify(options, null, 2))
} catch (e) {
@@ -218,21 +218,21 @@ module.exports = class Creator {
)
}
}
resolvePkg () {
const sortDeps = deps => Object.keys(deps).sort().reduce((res, name) => {
res[name] = deps[name]
return res
}, {})
this.pkg.dependencies = sortDeps(this.pkg.dependencies)
this.pkg.devDependencies = sortDeps(this.pkg.devDependencies)
debug('pkg')(this.pkg)
}
async resolveFiles () {
for (const middleware of this.fileMiddlewares) {
await middleware(this.files, ejs.render)
}
debug('files')(this.files)
}
}
function updatePackageForDev (targetDir, deps) {
const pkg = require(path.resolve(targetDir, 'package.json'))
pkg.devDependencies = {}
deps.forEach(dep => {
pkg.devDependencies[dep] = require(path.resolve(
__dirname,
'../../../',
dep,
'package.json'
)).version
})
fs.writeFileSync(
path.resolve(targetDir, 'package.json'),
JSON.stringify(pkg, null, 2)
)
}

View File

@@ -0,0 +1,56 @@
const ejs = require('ejs')
const path = require('path')
const debug = require('debug')
const resolve = require('resolve')
const GeneratorAPI = require('./GeneratorAPI')
const { success } = require('@vue/cli-shared-utils')
const writeFileTree = require('./util/writeFileTree')
module.exports = class Generator {
constructor (context, options) {
this.context = context
this.options = options
this.pkg = require(path.resolve(context, 'package.json'))
// for conflict resolution
this.depSources = {}
// virtual file tree
this.files = {}
this.fileMiddlewares = []
// apply generators from plugins
Object.keys(options.plugins).forEach(id => {
const generatorPath = resolve.sync(`${id}/generator`, { basedir: context })
const generator = require(generatorPath)
const generatorOptions = options.plugins[id]
generator(new GeneratorAPI(id, this, generatorOptions), generatorOptions)
})
}
async generate () {
// wait for file resolve
await this.resolveFiles()
// set package.json
this.resolvePkg()
this.files['package.json'] = JSON.stringify(this.pkg, null, 2)
// write file tree to disk
await writeFileTree(this.context, this.files)
success(`Successfully generated project files.`)
}
resolvePkg () {
const sortDeps = deps => Object.keys(deps).sort().reduce((res, name) => {
res[name] = deps[name]
return res
}, {})
this.pkg.dependencies = sortDeps(this.pkg.dependencies)
this.pkg.devDependencies = sortDeps(this.pkg.devDependencies)
debug('pkg')(this.pkg)
}
async resolveFiles () {
for (const middleware of this.fileMiddlewares) {
await middleware(this.files, ejs.render)
}
debug('files')(this.files)
}
}

View File

@@ -5,54 +5,24 @@ const walk = require('klaw-sync')
const isBinary = require('isbinaryfile')
const mergeDeps = require('./util/mergeDeps')
const errorParser = require('error-stack-parser')
const { error } = require('@vue/cli-shared-utils')
const isString = val => typeof val === 'string'
const isFunction = val => typeof val === 'function'
const isObject = val => val && typeof val === 'object'
module.exports = class GeneratorAPI {
constructor (id, creator) {
constructor (id, generator, options) {
this.id = id
this.creator = creator
}
injectFeature (feature) {
this.creator.featurePrompt.choices.push(feature)
}
injectPrompt (prompt) {
this.creator.injectedPrompts.push(prompt)
}
injectOptionForPrompt (name, option) {
const prompt = this.creator.injectedPrompts.find(f => {
return f.name === name
})
if (!prompt) {
error(
`injectOptionForFeature error in generator "${
this.id
}": prompt "${name}" does not exist.`
)
}
prompt.choices.push(option)
}
onPromptComplete (cb) {
this.creator.promptCompleteCbs.push(cb)
}
onCreateComplete (cb) {
this.creator.onCreateCompleteCbs.push(cb)
this.generator = generator
this.options = options
}
injectFileMiddleware (middleware) {
this.creator.fileMiddlewares.push(middleware)
this.generator.fileMiddlewares.push(middleware)
}
extendPackage (fields, options = { merge: true }) {
const pkg = this.creator.pkg
const pkg = this.generator.pkg
const toMerge = isFunction(fields) ? fields(pkg) : fields
for (const key in toMerge) {
if (!options.merge || !(key in pkg)) {
@@ -69,7 +39,7 @@ module.exports = class GeneratorAPI {
this.id,
existing,
value,
this.creator.depSources
this.generator.depSources
)
} else {
pkg[key] = Object.assign({}, existing, value)
@@ -87,7 +57,7 @@ module.exports = class GeneratorAPI {
fileDir = path.resolve(baseDir, fileDir)
this.injectFileMiddleware(files => {
const data = Object.assign({
options: this.creator.options
options: this.options
}, additionalData)
const _files = walk(fileDir, {
nodir: true,
@@ -101,7 +71,7 @@ module.exports = class GeneratorAPI {
} else if (isObject(fileDir)) {
this.injectFileMiddleware(files => {
const data = Object.assign({
options: this.creator.options
options: this.options
}, additionalData)
for (const targetPath in fileDir) {
const sourcePath = path.resolve(baseDir, fileDir[targetPath])

View File

@@ -0,0 +1,23 @@
module.exports = class PromptModuleAPI {
constructor (creator) {
this.creator = creator
}
injectFeature (feature) {
this.creator.featurePrompt.choices.push(feature)
}
injectPrompt (prompt) {
this.creator.injectedPrompts.push(prompt)
}
injectOptionForPrompt (name, option) {
this.creator.injectedPrompts.find(f => {
return f.name === name
}).choices.push(option)
}
onPromptComplete (cb) {
this.creator.promptCompleteCbs.push(cb)
}
}

View File

@@ -5,9 +5,7 @@ const rimraf = require('rimraf')
const inquirer = require('inquirer')
const program = require('commander')
const Creator = require('./Creator')
const debug = require('debug')('create')
const { warn, error } = require('@vue/cli-shared-utils')
const resolveInstalledGenerators = require('./util/resolveInstalledGenerators')
const { warn, error, clearConsole } = require('@vue/cli-shared-utils')
async function run () {
program
@@ -23,39 +21,33 @@ async function run () {
const targetDir = path.resolve(process.cwd(), projectName)
if (fs.existsSync(targetDir)) {
const { overwrite } = await inquirer.prompt([
clearConsole()
const { action } = await inquirer.prompt([
{
name: 'overwrite',
type: 'confirm',
message: `Target directory ${chalk.cyan(targetDir)} already exists.\n Overwrite?`
name: 'action',
type: 'list',
message: `Target directory ${chalk.cyan(targetDir)} already exists. Pick an action:`,
choices: [
{ name: 'Overwrite', value: 'overwrite' },
{ name: 'Merge', value: 'merge' },
{ name: 'Cancel', value: false }
]
}
])
if (overwrite) {
rimraf.sync(targetDir)
} else {
if (!action) {
return
} else if (action === 'overwrite') {
rimraf.sync(targetDir)
}
}
const createGenerator = (id, requirePath = id) => ({
id,
apply: require(requirePath)
})
const promptModules = fs
.readdirSync(path.resolve(__dirname, './promptModules'))
.filter(file => file.charAt(0) !== '.')
.map(file => require(`./promptModules/${file}`))
const builtInGenerators = fs
.readdirSync(path.resolve(__dirname, './generators'))
.filter(dir => dir.charAt(0) !== '.')
.map(id => createGenerator(id, `./generators/${id}`))
debug(builtInGenerators)
const installedGenerators = resolveInstalledGenerators().map(id => {
return createGenerator(id)
})
const creator = new Creator(projectName, builtInGenerators.concat(installedGenerators))
await creator.create(targetDir)
const creator = new Creator(promptModules)
await creator.create(projectName, targetDir)
}
run().catch(error)

View File

@@ -1,15 +0,0 @@
module.exports = api => {
api.onPromptComplete(options => {
if (!options.features.includes('ts') && !options.features.includes('-babel')) {
api.extendPackage({
devDependencies: {
'@vue/cli-plugin-babel': '^0.1.0',
'babel-preset-vue-app': '^2.0.0'
},
babel: {
presets: ['vue-app'] // TODO update babel-preset-vue-app
}
})
}
})
}

View File

@@ -1,31 +0,0 @@
module.exports = api => {
api.onPromptComplete(options => {
if (!options.features.includes('-core')) {
api.renderFiles('./files')
api.extendPackage({
scripts: {
'dev': 'vue-cli-service serve',
'build': 'vue-cli-service build',
'start': 'vue-cli-service serve --prod'
},
dependencies: {
'vue': '^2.5.13'
},
devDependencies: {
'@vue/cli-service': '^0.1.0',
'vue-template-compiler': '^2.5.13'
},
'postcss': {
'plugins': {
'autoprefixer': {}
}
},
browserslist: [
'> 1%',
'last 2 versions',
'not ie <= 8'
]
})
}
})
}

View File

@@ -1,3 +0,0 @@
module.exports = api => {
}

View File

@@ -1,3 +0,0 @@
module.exports = api => {
}

View File

@@ -1,3 +0,0 @@
module.exports = api => {
}

View File

@@ -1,3 +0,0 @@
module.exports = api => {
}

View File

@@ -1,3 +0,0 @@
module.exports = api => {
}

View File

@@ -1,3 +0,0 @@
module.exports = api => {
}

View File

@@ -0,0 +1,2 @@
module.exports = cli => {
}

View File

@@ -0,0 +1,2 @@
module.exports = cli => {
}

View File

@@ -0,0 +1,2 @@
module.exports = cli => {
}

View File

@@ -0,0 +1,2 @@
module.exports = cli => {
}

View File

@@ -0,0 +1,2 @@
module.exports = cli => {
}

View File

@@ -1,13 +1,13 @@
module.exports = api => {
api.injectFeature({
module.exports = cli => {
cli.injectFeature({
name: 'Unit Testing',
value: 'unit',
short: 'Unit'
})
api.injectPrompt({
cli.injectPrompt({
name: 'unit',
when: options => options.features.includes('unit'),
when: answers => answers.features.includes('unit'),
type: 'list',
message: 'Pick a unit testing solution:',
choices: [
@@ -24,10 +24,10 @@ module.exports = api => {
]
})
api.injectPrompt({
cli.injectPrompt({
name: 'assertionLibrary',
message: 'Pick an assertion library for unit tests:',
when: options => options.unit === 'mocha',
when: answers => answers.unit === 'mocha',
type: 'list',
choices: [
{
@@ -48,11 +48,13 @@ module.exports = api => {
]
})
api.onPromptComplete(options => {
if (options.unit === 'mocha') {
require('./mocha-webpack')(api, options)
} else if (options.unit === 'jest') {
require('./jest')(api, options)
cli.onPromptComplete((answers, options) => {
if (answers.unit === 'mocha') {
options.plugins['@vue/cli-plugin-unit-mocha-webpack'] = {
assertionLibrary: answers.assertionLibrary
}
} else if (answers.unit === 'jest') {
options.plugins['@vue/cli-plugin-unit-jest'] = {}
}
})
}

View File

@@ -0,0 +1,2 @@
module.exports = cli => {
}

View File

@@ -1,3 +1,36 @@
module.exports = async function installDeps (command, targetDir) {
// TODO think about dep strategy
const { spawn } = require('child_process')
module.exports = function installDeps (command, targetDir, deps) {
return new Promise((resolve, reject) => {
const args = []
if (command === 'npm') {
args.push('install', '--loglevel', 'error')
if (deps) {
args.push('--save-dev')
}
} else if (command === 'yarn') {
if (deps) {
args.push('add', '--dev')
}
args.push('--silent')
} else {
throw new Error(`unknown package manager: ${command}`)
}
if (deps) {
args.push.apply(args, deps)
}
const child = spawn(command, args, {
cwd: targetDir,
stdio: 'inherit'
})
child.on('close', code => {
if (code !== 0) {
return reject(
`command failed: ${command} ${args.join(' ')}`
)
}
resolve()
})
})
}

View File

@@ -1,4 +0,0 @@
module.exports = function resolveInstalledGenerators () {
// TODO
return []
}

View File

@@ -3,9 +3,6 @@ const path = require('path')
const mkdirp = require('mkdirp')
module.exports = function writeFileTree (dir, files) {
if (process.env.DEBUG) {
return
}
for (const name in files) {
const filePath = path.join(dir, name)
mkdirp.sync(path.dirname(filePath))

View File

@@ -35,6 +35,7 @@
"isbinaryfile": "^3.0.2",
"klaw-sync": "^3.0.2",
"mkdirp": "^0.5.1",
"resolve": "^1.5.0",
"rimraf": "^2.6.2",
"semver": "^5.4.1"
},

View File

@@ -6061,7 +6061,7 @@ resolve@1.1.7:
version "1.1.7"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
resolve@^1.4.0:
resolve@^1.4.0, resolve@^1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.5.0.tgz#1f09acce796c9a762579f31b2c1cc4c3cddf9f36"
dependencies: