This commit is contained in:
Evan You
2017-12-25 16:54:49 -05:00
parent 763dd35fa0
commit c19eafce9e
14 changed files with 188 additions and 113 deletions

View File

@@ -3,7 +3,8 @@
"workspaces": [
"packages/@vue/*"
],
"devDependencies": {
"lerna": "^2.5.1"
}
"devDependencies": {
"debug": "^3.1.0",
"lerna": "^2.5.1"
}
}

View File

@@ -7,6 +7,7 @@ const { execSync } = require('child_process')
const GeneratorAPI = require('./GeneratorAPI')
const writeFileTree = require('./util/writeFileTree')
const debug = require('debug')
const rcPath = path.join(os.homedir(), '.vuerc')
const isMode = _mode => ({ mode }) => _mode === mode
@@ -19,21 +20,27 @@ const defaultOptions = {
module.exports = class Creator {
constructor (name, generators) {
this.name = name
const { modePrompt, featurePrompt } = this.resolveIntroPrompts()
this.modePrompt = modePrompt
this.featurePrompt = featurePrompt
this.outroPrompts = this.resolveOutroPrompts()
this.injectedPrompts = []
this.deps = {}
this.devDeps = {}
this.scripts = {}
this.packageFields = {}
this.files = {}
this.postCreateMessages = []
this.promptCompleteCbs = []
this.fileMiddlewares = []
this.pkg = {
name,
version: '0.1.0',
private: true,
scripts: {},
dependencies: {},
devDependencies: {}
}
// for conflict resolution
this.depSources = {}
// virtual file tree
this.files = {}
generators.forEach(generator => {
generator.module(new GeneratorAPI(this, generator))
})
@@ -42,29 +49,28 @@ module.exports = class Creator {
async create (path) {
// prompt
let options = await inquirer.prompt(this.resolveFinalPrompts())
let needSave = false
debug('rawOptions')(options)
if (options.mode === 'saved') {
options = this.loadSavedOptions()
} else if (options.mode === 'default') {
options = defaultOptions
} else if (options.save) {
needSave = true
}
options.features = options.features || []
// run cbs (register generators)
// run cb registered by generators
this.promptCompleteCbs.forEach(cb => cb(options))
// save after prompt complete cbs are run, since generators may modify
// options in the callback
if (needSave) {
debug('options')(options)
// save options
if (options.mode === 'manual' && options.save) {
this.saveOptions(options)
}
// resolve deps, scripts and generate final package.json
this.resolvePackage()
// 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(path, this.files)
}
@@ -156,14 +162,14 @@ module.exports = class Creator {
return options.mode === 'manual' && originalWhen(options)
}
})
const ret = [].concat(
const prompts = [].concat(
this.modePrompt,
this.featurePrompt,
this.injectedPrompts,
this.outroPrompts
)
console.log(ret)
return ret
debug('prompts')(prompts)
return prompts
}
loadSavedOptions () {
@@ -191,45 +197,21 @@ module.exports = class Creator {
}
}
resolvePackage () {
const { dependencies, devDependencies } = this.resolveDeps()
const scripts = this.resolveScripts()
const additionalFields = this.resolvePackageFields()
const pkg = Object.assign({}, additionalFields, {
name: this.name,
version: '0.1.0',
private: true,
scripts,
dependencies,
devDependencies
})
this.files['package.json'] = JSON.stringify(pkg, null, 2)
}
// TODO
resolveDeps () {
const dependencies = this.deps
const devDependencies = this.devDeps
return {
dependencies,
devDependencies
}
}
// TODO
resolveScripts () {
return this.scripts
}
// TODO
resolvePackageFields () {
return this.packageFields
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)
}
debug('files')(this.files)
}
}

View File

@@ -1,4 +1,7 @@
const { error } = require('./util/log')
const mergeDeps = require('./util/mergeDeps')
const isObject = val => val && typeof val === 'object'
const isFunction = val => typeof val === 'function'
module.exports = class GeneratorAPI {
constructor (creator, generator) {
@@ -32,31 +35,47 @@ module.exports = class GeneratorAPI {
this.creator.promptCompleteCbs.push(cb)
}
injectDeps (deps) {
Object.assign(this.creator.deps, deps)
}
injectDevDeps(deps) {
Object.assign(this.creator.devDeps, deps)
}
injectScripts (scripts) {
Object.assign(this.creator.scripts, scripts)
}
injectPackageFields (fields) {
Object.assign(this.creator.packageFields, fields)
onCreateComplete (msg) {
this.creator.onCreateCompleteCbs.push(cb)
}
injectFileMiddleware (middleware) {
this.creator.fileMiddlewares.push(middleware)
}
renderFile (file) {
return file
extendPackage (fields, options = { merge: true }) {
const pkg = this.creator.pkg
const toMerge = isFunction(fields) ? fields(pkg) : fields
for (const key in toMerge) {
if (!options.merge || !(key in pkg)) {
pkg[key] = toMerge[key]
} else {
const value = toMerge[key]
const existing = pkg[key]
if (Array.isArray(value) && Array.isArray(existing)) {
pkg[key] = existing.concat(value)
} else if (isObject(value) && isObject(existing)) {
if (key === 'dependencies' || key === 'devDependencies') {
// use special version resolution merge
pkg[key] = mergeDeps(
this.generator.id,
existing,
value,
this.creator.depSources
)
} else {
pkg[key] = Object.assign({}, existing, value)
}
} else {
pkg[key] = value
}
}
}
}
onCreateComplete (msg) {
this.creator.onCreateCompleteCbs.push(cb)
renderFile (file, additionalData, ejsOptions) {
// TODO render file based on generator path
// render with ejs & options
return file
}
}

View File

@@ -2,6 +2,7 @@ const fs = require('fs')
const path = require('path')
const program = require('commander')
const Creator = require('./Creator')
const debug = require('debug')('create')
const Generator = require('./Generator')
const { warn, error } = require('./util/log')
const resolveInstalledGenerators = require('./util/resolveInstalledGenerators')
@@ -21,6 +22,8 @@ const builtInGenerators = fs
.readdirSync(path.resolve(__dirname, './generators'))
.map(id => new Generator(id, `./generators/${id}`))
debug(builtInGenerators)
const installedGenerators = resolveInstalledGenerators().map(id => {
return new Generator(id)
})

View File

@@ -1,9 +1,11 @@
module.exports = api => {
api.onPromptComplete(options => {
if (!options.features.includes('ts')) {
api.injectDevDeps({
'@vue/cli-plugin-babel': '^1.0.0',
'babel-preset-vue-app': '^2.0.0'
api.extendPackage({
devDependencies: {
'@vue/cli-plugin-babel': '^1.0.0',
'babel-preset-vue-app': '^2.0.0'
}
})
api.injectFileMiddleware(files => {
files['.babelrc'] = api.renderFile('.babelrc')

View File

@@ -1,11 +1,11 @@
import { shallow } from 'vue-test-utils'
import Hello from '@/components/Hello.vue'
<% if (assertionLibrary === 'expect') { %>
<%_ if (assertionLibrary === 'expect') { _%>
import { expect } from 'expect'
<% } %>
<% if (assertionLibrary === 'chai') { %>
<%_ } _%>
<%_ if (assertionLibrary === 'chai') { _%>
import { expect } from 'chai'
<% } %>
<%_ } _%>
import Hello from '@/components/Hello.vue'
describe('Hello.vue', () => {
it('renders props.msg when passed', () => {
@@ -13,14 +13,14 @@ describe('Hello.vue', () => {
const wrapper = shallow(Hello, {
context: { props: { msg } }
})
<% if (assertionLibrary === 'expect' || unit === 'jest') { %>
<%_ if (assertionLibrary === 'expect' || unit === 'jest') { _%>
expect(wrapper.text()).toBe(msg)
<% } %>
<% if (assertionLibrary === 'chai') { %>
<%_ } _%>
<%_ if (assertionLibrary === 'chai') { _%>
expect(wrapper.text()).to.equal(msg)
<% } %>
<% if (assertionLibrary === 'custom') { %>
<%_ } _%>
<%_ if (assertionLibrary === 'custom') { _%>
// assert wrapper.text() equals msg
<% } %>
<%_ } _%>
})
})

View File

@@ -1,15 +1,13 @@
module.exports = (api, options) => {
api.injectDevDeps({
'@vue/cli-plugin-unit-jest': '^1.0.0',
"jest": "^22.0.4",
'vue-test-utils': '^1.0.0'
})
api.injectScripts({
test: 'jest'
})
api.injectPackageFields({
api.extendPackage({
scripts: {
test: 'jest'
},
devDependencies: {
'@vue/cli-plugin-unit-jest': '^1.0.0',
"jest": "^22.0.4",
'vue-test-utils': '^1.0.0'
},
"jest": {
"moduleFileExtensions": [
"js",

View File

@@ -1,17 +1,19 @@
module.exports = (api, options) => {
const dependencies = {
const devDependencies = {
'@vue/cli-plugin-unit-mocha-webpack': '^1.0.0',
'vue-test-utils': '^1.0.0'
}
if (options.assertionLibrary === 'chai') {
dependencies.chai = '^4.1.2'
devDependencies.chai = '^4.1.2'
} else if (options.assertionLibrary === 'expect') {
dependencies.expect = '^22.0.3'
devDependencies.expect = '^22.0.3'
}
api.injectDevDeps(dependencies)
api.injectScripts({
test: 'vue-cli-service test'
api.extendPackage({
devDependencies,
scripts: {
test: 'vue-cli-service test'
}
})
api.injectFileMiddleware(files => {

View File

@@ -1,21 +1,25 @@
const chalk = require('chalk')
exports.info = msg => {
console.log(chalk.gray(`\n ${msg}\n`))
const format = (label, msg) => {
return msg.split('\n').map((line, i) => {
return i === 0
? `\n ${label} ${line}`
: ` ${line}`
}).join('\n') + '\n'
}
exports.success = msg => {
console.log(chalk.green(`\n ${msg}\n`))
console.log(format(chalk.bgGreen(' OK '), chalk.green(msg)))
}
exports.warn = msg => {
console.warn(chalk.yellow(`\n ${msg}\n`))
console.warn(format(chalk.bgYellow(chalk.black(' WARN ')), chalk.yellow(msg)))
}
exports.error = msg => {
console.error(`\n ${chalk.bgRed(' ERROR ')} ${chalk.red(msg)}\n`)
console.error(format(chalk.bgRed(' ERROR '), chalk.red(msg)))
if (msg instanceof Error) {
console.log(msg.stack)
console.error(msg.stack)
}
process.exit(1)
}

View File

@@ -0,0 +1,54 @@
const semver = require('semver')
const { warn } = require('./log')
module.exports = function resolveDeps (generatorId, to, from, sources) {
const res = Object.assign({}, to)
for (const name in from) {
const r1 = to[name]
const r2 = from[name]
const sourceGeneratorId = sources[name]
if (!semver.validRange(r2)) {
warn(
`invalid version range for dependency "${name}":\n\n` +
`- ${r2} injected by generator "${generatorId}"`
)
continue
}
if (!r1) {
res[name] = r2
} else {
const r = tryGetNewerRange(r1, r2)
const didGetNewer = !!r
// if failed to infer newer version, use existing one because it's likely
// built-in
res[name] = didGetNewer ? r : r1
// if changed, update source
if (res[name] === r2) {
sources[name] = generatorId
}
// warn incompatible version requirements
if (!semver.intersects(r1, r2)) {
warn(
`conflicting versions for project dependency "${name}":\n\n` +
`- ${r1} injected by generator "${sourceGeneratorId}"\n` +
`- ${r2} injected by generator "${generatorId}"\n\n` +
`Using ${didGetNewer ? `newer ` : ``}version (${res[name]}), but this may cause build errors.`
)
}
}
}
return res
}
const leadRE = /^(~|\^|>=?)/
const rangeToVersion = r => r.replace(leadRE, '').replace(/x/g, '0')
function tryGetNewerRange (r1, r2) {
const v1 = rangeToVersion(r1)
const v2 = rangeToVersion(r2)
if (semver.valid(v1) && semver.valid(v2)) {
return semver.gt(v1, v2) ? r1 : r2
}
}

View File

@@ -1,3 +1,2 @@
module.exports = function writeFileTree (dir, files) {
console.log(files)
}

View File

@@ -23,6 +23,7 @@
"dependencies": {
"chalk": "^2.3.0",
"commander": "^2.12.2",
"inquirer": "^4.0.1"
"inquirer": "^4.0.1",
"semver": "^5.4.1"
}
}

View File

@@ -412,6 +412,12 @@ dateformat@^1.0.11, dateformat@^1.0.12:
get-stdin "^4.0.1"
meow "^3.3.0"
debug@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
dependencies:
ms "2.0.0"
decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2:
version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
@@ -1049,6 +1055,10 @@ moment@^2.6.0:
version "2.20.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.20.1.tgz#d6eb1a46cbcc14a2b2f9434112c1ff8907f313fd"
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
mute-stream@0.0.7:
version "0.0.7"
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"