diff --git a/package.json b/package.json index 3a72cbfdc..e1b9ff890 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,8 @@ "workspaces": [ "packages/@vue/*" ], - "devDependencies": { - "lerna": "^2.5.1" - } + "devDependencies": { + "debug": "^3.1.0", + "lerna": "^2.5.1" + } } diff --git a/packages/@vue/cli/lib/Creator.js b/packages/@vue/cli/lib/Creator.js index 0bff6be25..d3fee8894 100644 --- a/packages/@vue/cli/lib/Creator.js +++ b/packages/@vue/cli/lib/Creator.js @@ -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) } } diff --git a/packages/@vue/cli/lib/GeneratorAPI.js b/packages/@vue/cli/lib/GeneratorAPI.js index 52af53a1c..9290c703a 100644 --- a/packages/@vue/cli/lib/GeneratorAPI.js +++ b/packages/@vue/cli/lib/GeneratorAPI.js @@ -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 } } diff --git a/packages/@vue/cli/lib/create.js b/packages/@vue/cli/lib/create.js index 51ac83aa8..6ea65599b 100644 --- a/packages/@vue/cli/lib/create.js +++ b/packages/@vue/cli/lib/create.js @@ -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) }) diff --git a/packages/@vue/cli/lib/generators/babel/index.js b/packages/@vue/cli/lib/generators/babel/index.js index 36e6e5c16..ba9f7a255 100644 --- a/packages/@vue/cli/lib/generators/babel/index.js +++ b/packages/@vue/cli/lib/generators/babel/index.js @@ -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') diff --git a/packages/@vue/cli/lib/generators/unit/files/Hello.spec.js b/packages/@vue/cli/lib/generators/unit/files/Hello.spec.js index 06df0b74f..f804b5098 100644 --- a/packages/@vue/cli/lib/generators/unit/files/Hello.spec.js +++ b/packages/@vue/cli/lib/generators/unit/files/Hello.spec.js @@ -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 - <% } %> + <%_ } _%> }) }) diff --git a/packages/@vue/cli/lib/generators/unit/jest.js b/packages/@vue/cli/lib/generators/unit/jest.js index 930c08ec9..0167907a2 100644 --- a/packages/@vue/cli/lib/generators/unit/jest.js +++ b/packages/@vue/cli/lib/generators/unit/jest.js @@ -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", diff --git a/packages/@vue/cli/lib/generators/unit/mocha-webpack.js b/packages/@vue/cli/lib/generators/unit/mocha-webpack.js index 82328f67e..a754951fb 100644 --- a/packages/@vue/cli/lib/generators/unit/mocha-webpack.js +++ b/packages/@vue/cli/lib/generators/unit/mocha-webpack.js @@ -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 => { diff --git a/packages/@vue/cli/lib/util/log.js b/packages/@vue/cli/lib/util/log.js index 8d9448792..05634501e 100644 --- a/packages/@vue/cli/lib/util/log.js +++ b/packages/@vue/cli/lib/util/log.js @@ -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) } diff --git a/packages/@vue/cli/lib/util/mergeDeps.js b/packages/@vue/cli/lib/util/mergeDeps.js new file mode 100644 index 000000000..0ddb3b38d --- /dev/null +++ b/packages/@vue/cli/lib/util/mergeDeps.js @@ -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 + } +} diff --git a/packages/@vue/cli/lib/util/resolveDeps.js b/packages/@vue/cli/lib/util/resolveDeps.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/@vue/cli/lib/util/writeFileTree.js b/packages/@vue/cli/lib/util/writeFileTree.js index d5d44846b..6036bb8f2 100644 --- a/packages/@vue/cli/lib/util/writeFileTree.js +++ b/packages/@vue/cli/lib/util/writeFileTree.js @@ -1,3 +1,2 @@ module.exports = function writeFileTree (dir, files) { - console.log(files) } diff --git a/packages/@vue/cli/package.json b/packages/@vue/cli/package.json index 35854b17d..2a5fb3e6b 100644 --- a/packages/@vue/cli/package.json +++ b/packages/@vue/cli/package.json @@ -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" } } diff --git a/yarn.lock b/yarn.lock index f06304cb6..ec7018c96 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"