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
+4 -3
View File
@@ -3,7 +3,8 @@
"workspaces": [ "workspaces": [
"packages/@vue/*" "packages/@vue/*"
], ],
"devDependencies": { "devDependencies": {
"lerna": "^2.5.1" "debug": "^3.1.0",
} "lerna": "^2.5.1"
}
} }
+35 -53
View File
@@ -7,6 +7,7 @@ const { execSync } = require('child_process')
const GeneratorAPI = require('./GeneratorAPI') const GeneratorAPI = require('./GeneratorAPI')
const writeFileTree = require('./util/writeFileTree') const writeFileTree = require('./util/writeFileTree')
const debug = require('debug')
const rcPath = path.join(os.homedir(), '.vuerc') const rcPath = path.join(os.homedir(), '.vuerc')
const isMode = _mode => ({ mode }) => _mode === mode const isMode = _mode => ({ mode }) => _mode === mode
@@ -19,21 +20,27 @@ const defaultOptions = {
module.exports = class Creator { module.exports = class Creator {
constructor (name, generators) { constructor (name, generators) {
this.name = name
const { modePrompt, featurePrompt } = this.resolveIntroPrompts() const { modePrompt, featurePrompt } = this.resolveIntroPrompts()
this.modePrompt = modePrompt this.modePrompt = modePrompt
this.featurePrompt = featurePrompt this.featurePrompt = featurePrompt
this.outroPrompts = this.resolveOutroPrompts() this.outroPrompts = this.resolveOutroPrompts()
this.injectedPrompts = [] this.injectedPrompts = []
this.deps = {}
this.devDeps = {}
this.scripts = {}
this.packageFields = {}
this.files = {}
this.postCreateMessages = []
this.promptCompleteCbs = [] this.promptCompleteCbs = []
this.fileMiddlewares = [] 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 => { generators.forEach(generator => {
generator.module(new GeneratorAPI(this, generator)) generator.module(new GeneratorAPI(this, generator))
}) })
@@ -42,29 +49,28 @@ module.exports = class Creator {
async create (path) { async create (path) {
// prompt // prompt
let options = await inquirer.prompt(this.resolveFinalPrompts()) let options = await inquirer.prompt(this.resolveFinalPrompts())
let needSave = false debug('rawOptions')(options)
if (options.mode === 'saved') { if (options.mode === 'saved') {
options = this.loadSavedOptions() options = this.loadSavedOptions()
} else if (options.mode === 'default') { } else if (options.mode === 'default') {
options = defaultOptions options = defaultOptions
} else if (options.save) {
needSave = true
} }
options.features = options.features || [] options.features = options.features || []
// run cbs (register generators) // run cb registered by generators
this.promptCompleteCbs.forEach(cb => cb(options)) this.promptCompleteCbs.forEach(cb => cb(options))
debug('options')(options)
// save after prompt complete cbs are run, since generators may modify // save options
// options in the callback if (options.mode === 'manual' && options.save) {
if (needSave) {
this.saveOptions(options) this.saveOptions(options)
} }
// resolve deps, scripts and generate final package.json
this.resolvePackage()
// wait for file resolve // wait for file resolve
await this.resolveFiles() await this.resolveFiles()
// set package.json
this.resolvePkg()
this.files['package.json'] = JSON.stringify(this.pkg, null ,2)
// write file tree to disk // write file tree to disk
await writeFileTree(path, this.files) await writeFileTree(path, this.files)
} }
@@ -156,14 +162,14 @@ module.exports = class Creator {
return options.mode === 'manual' && originalWhen(options) return options.mode === 'manual' && originalWhen(options)
} }
}) })
const ret = [].concat( const prompts = [].concat(
this.modePrompt, this.modePrompt,
this.featurePrompt, this.featurePrompt,
this.injectedPrompts, this.injectedPrompts,
this.outroPrompts this.outroPrompts
) )
console.log(ret) debug('prompts')(prompts)
return ret return prompts
} }
loadSavedOptions () { loadSavedOptions () {
@@ -191,45 +197,21 @@ module.exports = class Creator {
} }
} }
resolvePackage () { resolvePkg () {
const { dependencies, devDependencies } = this.resolveDeps() const sortDeps = deps => Object.keys(deps).sort().reduce((res, name) => {
const scripts = this.resolveScripts() res[name] = deps[name]
const additionalFields = this.resolvePackageFields() return res
const pkg = Object.assign({}, additionalFields, { }, {})
name: this.name, this.pkg.dependencies = sortDeps(this.pkg.dependencies)
version: '0.1.0', this.pkg.devDependencies = sortDeps(this.pkg.devDependencies)
private: true, debug('pkg')(this.pkg)
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
} }
async resolveFiles () { async resolveFiles () {
for (const middleware of this.fileMiddlewares) { for (const middleware of this.fileMiddlewares) {
await middleware(this.files) await middleware(this.files)
} }
debug('files')(this.files)
} }
} }
+37 -18
View File
@@ -1,4 +1,7 @@
const { error } = require('./util/log') 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 { module.exports = class GeneratorAPI {
constructor (creator, generator) { constructor (creator, generator) {
@@ -32,31 +35,47 @@ module.exports = class GeneratorAPI {
this.creator.promptCompleteCbs.push(cb) this.creator.promptCompleteCbs.push(cb)
} }
injectDeps (deps) { onCreateComplete (msg) {
Object.assign(this.creator.deps, deps) this.creator.onCreateCompleteCbs.push(cb)
}
injectDevDeps(deps) {
Object.assign(this.creator.devDeps, deps)
}
injectScripts (scripts) {
Object.assign(this.creator.scripts, scripts)
}
injectPackageFields (fields) {
Object.assign(this.creator.packageFields, fields)
} }
injectFileMiddleware (middleware) { injectFileMiddleware (middleware) {
this.creator.fileMiddlewares.push(middleware) this.creator.fileMiddlewares.push(middleware)
} }
renderFile (file) { extendPackage (fields, options = { merge: true }) {
return file 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) { renderFile (file, additionalData, ejsOptions) {
this.creator.onCreateCompleteCbs.push(cb) // TODO render file based on generator path
// render with ejs & options
return file
} }
} }
+3
View File
@@ -2,6 +2,7 @@ const fs = require('fs')
const path = require('path') const path = require('path')
const program = require('commander') const program = require('commander')
const Creator = require('./Creator') const Creator = require('./Creator')
const debug = require('debug')('create')
const Generator = require('./Generator') const Generator = require('./Generator')
const { warn, error } = require('./util/log') const { warn, error } = require('./util/log')
const resolveInstalledGenerators = require('./util/resolveInstalledGenerators') const resolveInstalledGenerators = require('./util/resolveInstalledGenerators')
@@ -21,6 +22,8 @@ const builtInGenerators = fs
.readdirSync(path.resolve(__dirname, './generators')) .readdirSync(path.resolve(__dirname, './generators'))
.map(id => new Generator(id, `./generators/${id}`)) .map(id => new Generator(id, `./generators/${id}`))
debug(builtInGenerators)
const installedGenerators = resolveInstalledGenerators().map(id => { const installedGenerators = resolveInstalledGenerators().map(id => {
return new Generator(id) return new Generator(id)
}) })
@@ -1,9 +1,11 @@
module.exports = api => { module.exports = api => {
api.onPromptComplete(options => { api.onPromptComplete(options => {
if (!options.features.includes('ts')) { if (!options.features.includes('ts')) {
api.injectDevDeps({ api.extendPackage({
'@vue/cli-plugin-babel': '^1.0.0', devDependencies: {
'babel-preset-vue-app': '^2.0.0' '@vue/cli-plugin-babel': '^1.0.0',
'babel-preset-vue-app': '^2.0.0'
}
}) })
api.injectFileMiddleware(files => { api.injectFileMiddleware(files => {
files['.babelrc'] = api.renderFile('.babelrc') files['.babelrc'] = api.renderFile('.babelrc')
@@ -1,11 +1,11 @@
import { shallow } from 'vue-test-utils' import { shallow } from 'vue-test-utils'
import Hello from '@/components/Hello.vue' <%_ if (assertionLibrary === 'expect') { _%>
<% if (assertionLibrary === 'expect') { %>
import { expect } from 'expect' import { expect } from 'expect'
<% } %> <%_ } _%>
<% if (assertionLibrary === 'chai') { %> <%_ if (assertionLibrary === 'chai') { _%>
import { expect } from 'chai' import { expect } from 'chai'
<% } %> <%_ } _%>
import Hello from '@/components/Hello.vue'
describe('Hello.vue', () => { describe('Hello.vue', () => {
it('renders props.msg when passed', () => { it('renders props.msg when passed', () => {
@@ -13,14 +13,14 @@ describe('Hello.vue', () => {
const wrapper = shallow(Hello, { const wrapper = shallow(Hello, {
context: { props: { msg } } context: { props: { msg } }
}) })
<% if (assertionLibrary === 'expect' || unit === 'jest') { %> <%_ if (assertionLibrary === 'expect' || unit === 'jest') { _%>
expect(wrapper.text()).toBe(msg) expect(wrapper.text()).toBe(msg)
<% } %> <%_ } _%>
<% if (assertionLibrary === 'chai') { %> <%_ if (assertionLibrary === 'chai') { _%>
expect(wrapper.text()).to.equal(msg) expect(wrapper.text()).to.equal(msg)
<% } %> <%_ } _%>
<% if (assertionLibrary === 'custom') { %> <%_ if (assertionLibrary === 'custom') { _%>
// assert wrapper.text() equals msg // assert wrapper.text() equals msg
<% } %> <%_ } _%>
}) })
}) })
+9 -11
View File
@@ -1,15 +1,13 @@
module.exports = (api, options) => { module.exports = (api, options) => {
api.injectDevDeps({ api.extendPackage({
'@vue/cli-plugin-unit-jest': '^1.0.0', scripts: {
"jest": "^22.0.4", test: 'jest'
'vue-test-utils': '^1.0.0' },
}) devDependencies: {
'@vue/cli-plugin-unit-jest': '^1.0.0',
api.injectScripts({ "jest": "^22.0.4",
test: 'jest' 'vue-test-utils': '^1.0.0'
}) },
api.injectPackageFields({
"jest": { "jest": {
"moduleFileExtensions": [ "moduleFileExtensions": [
"js", "js",
@@ -1,17 +1,19 @@
module.exports = (api, options) => { module.exports = (api, options) => {
const dependencies = { const devDependencies = {
'@vue/cli-plugin-unit-mocha-webpack': '^1.0.0', '@vue/cli-plugin-unit-mocha-webpack': '^1.0.0',
'vue-test-utils': '^1.0.0' 'vue-test-utils': '^1.0.0'
} }
if (options.assertionLibrary === 'chai') { if (options.assertionLibrary === 'chai') {
dependencies.chai = '^4.1.2' devDependencies.chai = '^4.1.2'
} else if (options.assertionLibrary === 'expect') { } else if (options.assertionLibrary === 'expect') {
dependencies.expect = '^22.0.3' devDependencies.expect = '^22.0.3'
} }
api.injectDevDeps(dependencies)
api.injectScripts({ api.extendPackage({
test: 'vue-cli-service test' devDependencies,
scripts: {
test: 'vue-cli-service test'
}
}) })
api.injectFileMiddleware(files => { api.injectFileMiddleware(files => {
+10 -6
View File
@@ -1,21 +1,25 @@
const chalk = require('chalk') const chalk = require('chalk')
exports.info = msg => { const format = (label, msg) => {
console.log(chalk.gray(`\n ${msg}\n`)) return msg.split('\n').map((line, i) => {
return i === 0
? `\n ${label} ${line}`
: ` ${line}`
}).join('\n') + '\n'
} }
exports.success = msg => { exports.success = msg => {
console.log(chalk.green(`\n ${msg}\n`)) console.log(format(chalk.bgGreen(' OK '), chalk.green(msg)))
} }
exports.warn = 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 => { 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) { if (msg instanceof Error) {
console.log(msg.stack) console.error(msg.stack)
} }
process.exit(1) process.exit(1)
} }
+54
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
}
}
@@ -1,3 +1,2 @@
module.exports = function writeFileTree (dir, files) { module.exports = function writeFileTree (dir, files) {
console.log(files)
} }
+2 -1
View File
@@ -23,6 +23,7 @@
"dependencies": { "dependencies": {
"chalk": "^2.3.0", "chalk": "^2.3.0",
"commander": "^2.12.2", "commander": "^2.12.2",
"inquirer": "^4.0.1" "inquirer": "^4.0.1",
"semver": "^5.4.1"
} }
} }
+10
View File
@@ -412,6 +412,12 @@ dateformat@^1.0.11, dateformat@^1.0.12:
get-stdin "^4.0.1" get-stdin "^4.0.1"
meow "^3.3.0" 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: decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
@@ -1049,6 +1055,10 @@ moment@^2.6.0:
version "2.20.1" version "2.20.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.20.1.tgz#d6eb1a46cbcc14a2b2f9434112c1ff8907f313fd" 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: mute-stream@0.0.7:
version "0.0.7" version "0.0.7"
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"