├── test └── e2e │ ├── mock-meta-json │ ├── template │ │ └── .gitkeep │ └── meta.json │ ├── mock-error │ ├── template │ │ └── readme.md │ └── meta.js │ ├── mock-metadata-repo-js │ ├── template │ │ └── readme.md │ └── meta.js │ ├── mock-skip-glob │ ├── template │ │ ├── src │ │ │ ├── yes.js │ │ │ ├── no.js │ │ │ ├── no.vue │ │ │ └── yes.vue │ │ └── package.json │ └── meta.json │ ├── mock-template-repo │ ├── template │ │ ├── src │ │ │ ├── no.js │ │ │ ├── skip-one.vue │ │ │ ├── skip-two.vue │ │ │ └── yes.vue │ │ └── package.json │ └── meta.json │ ├── mock-metalsmith-custom │ ├── template │ │ └── readme.md │ └── meta.js │ ├── mock-metalsmith-custom-before-after │ ├── template │ │ └── readme.md │ └── meta.js │ ├── mock-vue-app │ ├── config.js │ ├── index.js │ └── App.vue │ ├── test-build.js │ └── test.js ├── circle.yml ├── .gitignore ├── .babelrc ├── .eslintrc ├── .editorconfig ├── bin ├── vue ├── vue-list ├── vue-init └── vue-build ├── appveyor.yml ├── lib ├── template.html ├── default-entry.es6 ├── local-path.js ├── git-user.js ├── eval.js ├── filter.js ├── warnings.js ├── logger.js ├── check-version.js ├── server.js ├── ask.js ├── loaders.js ├── options.js ├── run.js └── generate.js ├── issue_template.md ├── CONTRIBUTING.md ├── LICENSE ├── package.json ├── docs └── build.md └── README.md /test/e2e/mock-meta-json/template/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 5 4 | -------------------------------------------------------------------------------- /test/e2e/mock-error/template/readme.md: -------------------------------------------------------------------------------- 1 | {{error,name}} 2 | -------------------------------------------------------------------------------- /test/e2e/mock-metadata-repo-js/template/readme.md: -------------------------------------------------------------------------------- 1 | {{ uppercase name}} -------------------------------------------------------------------------------- /test/e2e/mock-skip-glob/template/src/yes.js: -------------------------------------------------------------------------------- 1 | console.log('yes.') 2 | -------------------------------------------------------------------------------- /test/e2e/mock-skip-glob/template/src/no.js: -------------------------------------------------------------------------------- 1 | console.log('{{ no }}') 2 | -------------------------------------------------------------------------------- /test/e2e/mock-template-repo/template/src/no.js: -------------------------------------------------------------------------------- 1 | console.log('no.') 2 | -------------------------------------------------------------------------------- /test/e2e/mock-metalsmith-custom/template/readme.md: -------------------------------------------------------------------------------- 1 | Metalsmith {{custom}} 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | test/e2e/mock-template-build 4 | *.log 5 | dist/ 6 | -------------------------------------------------------------------------------- /test/e2e/mock-skip-glob/template/src/no.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /test/e2e/mock-skip-glob/template/src/yes.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /test/e2e/mock-template-repo/template/src/skip-one.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/e2e/mock-template-repo/template/src/skip-two.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/e2e/mock-template-repo/template/src/yes.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /test/e2e/mock-metalsmith-custom-before-after/template/readme.md: -------------------------------------------------------------------------------- 1 | Metalsmith {{after}} and {{before}} hooks 2 | -------------------------------------------------------------------------------- /test/e2e/mock-vue-app/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | autoprefixer: { 3 | browsers: ['ie > 10'] 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/e2e/mock-vue-app/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | 4 | new Vue({ 5 | el: '#app', 6 | render: h => h(App) 7 | }) 8 | -------------------------------------------------------------------------------- /test/e2e/mock-vue-app/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "mocha": true, 5 | "development": { 6 | "presets": ["es2015", "stage-2"] 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard", 3 | "env": { 4 | "mocha": true, 5 | "node": true 6 | }, 7 | "rules": { 8 | "arrow-parens": [2, "as-needed"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/e2e/mock-error/meta.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | prompts: { 3 | name: { 4 | type: 'string', 5 | required: true, 6 | message: 'Name' 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/e2e/mock-meta-json/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "completeMessage": "{{#inPlace}}To get started:\n\n npm install\n npm run dev.{{else}}To get started:\n\n cd {{destDirName}}\n npm install\n npm run dev.{{/inPlace}}" 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /bin/vue: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('commander') 4 | .version(require('../package').version) 5 | .usage(' [options]') 6 | .command('init', 'generate a new project from a template') 7 | .command('list', 'list available official templates') 8 | .command('build', 'prototype a new project') 9 | .parse(process.argv) 10 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - nodejs_version: "5" 4 | - nodejs_version: "6" 5 | 6 | install: 7 | - ps: Install-Product node $env:nodejs_version 8 | - npm install 9 | 10 | test_script: 11 | - node --version 12 | - npm --version 13 | - npm test 14 | 15 | cache: 16 | - node_modules -> yarn.lock 17 | 18 | build: off 19 | -------------------------------------------------------------------------------- /lib/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <%= htmlWebpackPlugin.options.title %> 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /lib/default-entry.es6: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import Vue from 'vue' 3 | // your-tasteful-component is an alias to the path of your component 4 | // for example: vue build component.vue 5 | // then `your-tasteful-component` is `component.vue` 6 | import App from 'your-tasteful-component' 7 | 8 | new Vue({ 9 | el: '#app', 10 | render: h => h(App) 11 | }) 12 | -------------------------------------------------------------------------------- /test/e2e/mock-skip-glob/template/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "{{ name }}", 3 | "description": "{{ description }}", 4 | "author": "{{ author }}", 5 | "devDependencies": { 6 | {{#preprocessor.less}} 7 | "less": "^2.6.1", 8 | {{/preprocessor.less}} 9 | {{#preprocessor.sass}} 10 | "node-sass": "^3.4.2" 11 | {{/preprocessor.sass}} 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/e2e/mock-template-repo/template/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "{{ name }}", 3 | "description": "{{ description }}", 4 | "author": "{{ author }}", 5 | "devDependencies": { 6 | {{#preprocessor.less}} 7 | "less": "^2.6.1", 8 | {{/preprocessor.less}} 9 | {{#preprocessor.sass}} 10 | "node-sass": "^3.4.2" 11 | {{/preprocessor.sass}} 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/local-path.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | 3 | module.exports = { 4 | isLocalPath: function (templatePath) { 5 | return /^[./]|(^[a-zA-Z]:)/.test(templatePath) 6 | }, 7 | 8 | getTemplatePath: function (templatePath) { 9 | return path.isAbsolute(templatePath) 10 | ? templatePath 11 | : path.normalize(path.join(process.cwd(), templatePath)) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/git-user.js: -------------------------------------------------------------------------------- 1 | var exec = require('child_process').execSync 2 | 3 | module.exports = function () { 4 | var name 5 | var email 6 | 7 | try { 8 | name = exec('git config --get user.name') 9 | email = exec('git config --get user.email') 10 | } catch (e) {} 11 | 12 | name = name && JSON.stringify(name.toString().trim()).slice(1, -1) 13 | email = email && (' <' + email.toString().trim() + '>') 14 | return (name || '') + (email || '') 15 | } 16 | -------------------------------------------------------------------------------- /lib/eval.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk') 2 | 3 | /** 4 | * Evaluate an expression in meta.json in the context of 5 | * prompt answers data. 6 | */ 7 | 8 | module.exports = function evaluate (exp, data) { 9 | /* eslint-disable no-new-func */ 10 | var fn = new Function('data', 'with (data) { return ' + exp + '}') 11 | try { 12 | return fn(data) 13 | } catch (e) { 14 | console.error(chalk.red('Error when evaluating filter condition: ' + exp)) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/e2e/mock-metalsmith-custom/meta.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "metalsmith": function (metalsmith, opts, helpers) { 3 | metalsmith.metadata().custom = "Custom"; 4 | function customMetalsmithPlugin (files, metalsmith, done) { 5 | // Implement something really custom here. 6 | 7 | var readme = files['readme.md'] 8 | delete files['readme.md'] 9 | files['custom/readme.md'] = readme 10 | 11 | done(null, files) 12 | } 13 | 14 | metalsmith.use(customMetalsmithPlugin) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/e2e/mock-metadata-repo-js/meta.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | prompts: { 3 | description: { 4 | type: 'string', 5 | required: true, 6 | message: 'Project description' 7 | }, 8 | name: { 9 | type: 'string', 10 | required: true, 11 | label: 'Project name', 12 | validate: function (input) { 13 | return input === 'custom' ? 'can not input `custom`' : true 14 | } 15 | } 16 | }, 17 | helpers: { 18 | uppercase: function (str) { 19 | return str.toUpperCase() 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /issue_template.md: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /lib/filter.js: -------------------------------------------------------------------------------- 1 | var match = require('minimatch') 2 | var evaluate = require('./eval') 3 | 4 | module.exports = function (files, filters, data, done) { 5 | if (!filters) { 6 | return done() 7 | } 8 | var fileNames = Object.keys(files) 9 | Object.keys(filters).forEach(function (glob) { 10 | fileNames.forEach(function (file) { 11 | if (match(file, glob, { dot: true })) { 12 | var condition = filters[glob] 13 | if (!evaluate(condition, data)) { 14 | delete files[file] 15 | } 16 | } 17 | }) 18 | }) 19 | done() 20 | } 21 | -------------------------------------------------------------------------------- /test/e2e/mock-skip-glob/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema": { 3 | "name": { 4 | "type": "string", 5 | "required": true, 6 | "label": "Project name" 7 | }, 8 | "description": { 9 | "type": "string", 10 | "required": true, 11 | "label": "Project description", 12 | "default": "A Vue.js project" 13 | }, 14 | "author": { 15 | "type": "string", 16 | "label": "Author" 17 | }, 18 | "private": { 19 | "type": "boolean", 20 | "default": true 21 | } 22 | }, 23 | "skipInterpolation": [ 24 | "src/**/*.{vue,js}", 25 | "!src/**/yes.*" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /test/e2e/mock-metalsmith-custom-before-after/meta.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "metalsmith": { 3 | before: function (metalsmith, opts, helpers) { 4 | metalsmith.metadata().before = "Before"; 5 | }, 6 | after: function (metalsmith, opts, helpers) { 7 | metalsmith.metadata().after = "After"; 8 | function customMetalsmithPlugin (files, metalsmith, done) { 9 | // Implement something really custom here. 10 | 11 | var readme = files['readme.md'] 12 | delete files['readme.md'] 13 | files['custom-before-after/readme.md'] = readme 14 | 15 | done(null, files) 16 | } 17 | 18 | metalsmith.use(customMetalsmithPlugin) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # vue-cli contributing guide 2 | 3 | ## Issue Reporting Guidelines 4 | 5 | - First identify where error is coming from. If it's occuring while running `vue` command then issue is indeed on 6 | `vue-cli` so please report it here. If error appears when you run one of `npm run` scripts, problem originates 7 | from a template you're using, [maybe one of the official ones](https://github.com/vuejs-templates). If so, please 8 | open an issue on a template repository. 9 | 10 | - Try to search for your issue, it may have already been answered or even fixed in the development branch. 11 | 12 | ## Development Setup 13 | 14 | ``` bash 15 | npm install 16 | bin/vue init 17 | bin/vue list 18 | ``` 19 | -------------------------------------------------------------------------------- /lib/warnings.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk') 2 | 3 | module.exports = { 4 | v2SuffixTemplatesDeprecated: function (template, name) { 5 | var initCommand = 'vue init ' + template.replace('-2.0', '') + ' ' + name 6 | 7 | console.log(chalk.red(' This template is deprecated, as the original template now uses Vue 2.0 by default.')) 8 | console.log() 9 | console.log(chalk.yellow(' Please use this command instead: ') + chalk.green(initCommand)) 10 | console.log() 11 | }, 12 | v2BranchIsNowDefault: function (template, name) { 13 | var vue1InitCommand = 'vue init ' + template + '#1.0' + ' ' + name 14 | 15 | console.log(chalk.green(' This will install Vue 2.x version of the template.')) 16 | console.log() 17 | console.log(chalk.yellow(' For Vue 1.x use: ') + chalk.green(vue1InitCommand)) 18 | console.log() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /bin/vue-list: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var logger = require('../lib/logger') 4 | var request = require('request') 5 | var chalk = require('chalk') 6 | 7 | /** 8 | * Padding. 9 | */ 10 | 11 | console.log() 12 | process.on('exit', function () { 13 | console.log() 14 | }) 15 | 16 | /** 17 | * List repos. 18 | */ 19 | 20 | request({ 21 | url: 'https://api.github.com/users/vuejs-templates/repos', 22 | headers: { 23 | 'User-Agent': 'vue-cli' 24 | } 25 | }, function (err, res, body) { 26 | if (err) logger.fatal(err) 27 | var requestBody = JSON.parse(body) 28 | if (Array.isArray(requestBody)) { 29 | console.log(' Available official templates:') 30 | console.log() 31 | requestBody.forEach(function (repo) { 32 | console.log( 33 | ' ' + chalk.yellow('★') + 34 | ' ' + chalk.blue(repo.name) + 35 | ' - ' + repo.description) 36 | }) 37 | } else { 38 | console.error(requestBody.message) 39 | } 40 | }) 41 | -------------------------------------------------------------------------------- /test/e2e/mock-template-repo/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema": { 3 | "name": { 4 | "type": "string", 5 | "required": true, 6 | "label": "Project name" 7 | }, 8 | "description": { 9 | "type": "string", 10 | "required": true, 11 | "label": "Project description", 12 | "default": "A Vue.js project" 13 | }, 14 | "author": { 15 | "type": "string", 16 | "label": "Author" 17 | }, 18 | "private": { 19 | "type": "boolean", 20 | "default": true 21 | }, 22 | "preprocessor": { 23 | "label": "CSS Preprocessor", 24 | "type": "checkbox", 25 | "choices": [ 26 | "less", 27 | "sass" 28 | ] 29 | }, 30 | "pick": { 31 | "type": "list", 32 | "choices": [ 33 | "yes", 34 | "no" 35 | ] 36 | } 37 | }, 38 | "filters": { 39 | "src/*.js": "pick === 'yes'", 40 | "**/*.vue": "pick === 'no'" 41 | }, 42 | "skipInterpolation": "src/*-{one,two}.vue" 43 | } 44 | -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk') 2 | var format = require('util').format 3 | 4 | /** 5 | * Prefix. 6 | */ 7 | 8 | var prefix = ' vue-cli' 9 | var sep = chalk.gray('·') 10 | 11 | /** 12 | * Log a `message` to the console. 13 | * 14 | * @param {String} message 15 | */ 16 | 17 | exports.log = function () { 18 | var msg = format.apply(format, arguments) 19 | console.log(chalk.white(prefix), sep, msg) 20 | } 21 | 22 | /** 23 | * Log an error `message` to the console and exit. 24 | * 25 | * @param {String} message 26 | */ 27 | 28 | exports.fatal = function (message) { 29 | if (message instanceof Error) message = message.message.trim() 30 | var msg = format.apply(format, arguments) 31 | console.error(chalk.red(prefix), sep, msg) 32 | process.exit(1) 33 | } 34 | 35 | /** 36 | * Log a success `message` to the console and exit. 37 | * 38 | * @param {String} message 39 | */ 40 | 41 | exports.success = function () { 42 | var msg = format.apply(format, arguments) 43 | console.log(chalk.white(prefix), sep, msg) 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2016 Evan You 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/check-version.js: -------------------------------------------------------------------------------- 1 | var request = require('request') 2 | var semver = require('semver') 3 | var chalk = require('chalk') 4 | var packageConfig = require('../package.json') 5 | 6 | module.exports = function (done) { 7 | // Ensure minimum supported node version is used 8 | if (!semver.satisfies(process.version, packageConfig.engines.node)) { 9 | return console.log(chalk.red( 10 | ' You must upgrade node to >=' + packageConfig.engines.node + '.x to use vue-cli' 11 | )) 12 | } 13 | 14 | request({ 15 | url: 'https://registry.npmjs.org/vue-cli', 16 | timeout: 1000 17 | }, function (err, res, body) { 18 | if (!err && res.statusCode === 200) { 19 | var latestVersion = JSON.parse(body)['dist-tags'].latest 20 | var localVersion = packageConfig.version 21 | if (semver.lt(localVersion, latestVersion)) { 22 | console.log(chalk.yellow(' A newer version of vue-cli is available.')) 23 | console.log() 24 | console.log(' latest: ' + chalk.green(latestVersion)) 25 | console.log(' installed: ' + chalk.red(localVersion)) 26 | console.log() 27 | } 28 | } 29 | done() 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var express = require('express') 3 | var proxyMiddleware = require('http-proxy-middleware') 4 | 5 | module.exports = function createServer (compiler, options) { 6 | var server = express() 7 | 8 | var devMiddleWare = require('webpack-dev-middleware')(compiler, { 9 | quiet: true 10 | }) 11 | 12 | server.use(devMiddleWare) 13 | server.use(require('webpack-hot-middleware')(compiler, { 14 | log: () => null 15 | })) 16 | server.use(require('connect-history-api-fallback')({index: '/'})) 17 | 18 | var mfs = devMiddleWare.fileSystem 19 | var file = path.join(compiler.options.output.path, 'index.html') 20 | 21 | // proxy api requests 22 | if (typeof options.proxy === 'string') { 23 | server.use(proxyMiddleware('/api', { 24 | target: options.proxy, 25 | changeOrigin: true, 26 | pathRewrite: { 27 | '^/api': '' 28 | } 29 | })) 30 | } else if (typeof options.proxy === 'object') { 31 | Object.keys(options.proxy).forEach(function (context) { 32 | var proxyOptions = options.proxy[context] 33 | if (typeof proxyOptions === 'string') { 34 | proxyOptions = { 35 | target: proxyOptions, 36 | changeOrigin: true, 37 | pathRewrite: { 38 | [`^${context}`]: '' 39 | } 40 | } 41 | } 42 | server.use(proxyMiddleware(context, proxyOptions)) 43 | }) 44 | } 45 | 46 | server.get('/', (req, res) => { 47 | devMiddleWare.waitUntilValid(() => { 48 | const html = mfs.readFileSync(file) 49 | res.end(html) 50 | }) 51 | }) 52 | 53 | if (options.setup) { 54 | options.setup(server) 55 | } 56 | 57 | return server 58 | } 59 | -------------------------------------------------------------------------------- /lib/ask.js: -------------------------------------------------------------------------------- 1 | var async = require('async') 2 | var inquirer = require('inquirer') 3 | var evaluate = require('./eval') 4 | 5 | // Support types from prompt-for which was used before 6 | var promptMapping = { 7 | string: 'input', 8 | boolean: 'confirm' 9 | } 10 | 11 | /** 12 | * Ask questions, return results. 13 | * 14 | * @param {Object} prompts 15 | * @param {Object} data 16 | * @param {Function} done 17 | */ 18 | 19 | module.exports = function ask (prompts, data, done) { 20 | async.eachSeries(Object.keys(prompts), function (key, next) { 21 | prompt(data, key, prompts[key], next) 22 | }, done) 23 | } 24 | 25 | /** 26 | * Inquirer prompt wrapper. 27 | * 28 | * @param {Object} data 29 | * @param {String} key 30 | * @param {Object} prompt 31 | * @param {Function} done 32 | */ 33 | 34 | function prompt (data, key, prompt, done) { 35 | // skip prompts whose when condition is not met 36 | if (prompt.when && !evaluate(prompt.when, data)) { 37 | return done() 38 | } 39 | 40 | var promptDefault = prompt.default 41 | if (typeof prompt.default === 'function') { 42 | promptDefault = function () { 43 | return prompt.default.bind(this)(data) 44 | } 45 | } 46 | 47 | inquirer.prompt([{ 48 | type: promptMapping[prompt.type] || prompt.type, 49 | name: key, 50 | message: prompt.message || prompt.label || key, 51 | default: promptDefault, 52 | choices: prompt.choices || [], 53 | validate: prompt.validate || function () { return true } 54 | }], function (answers) { 55 | if (Array.isArray(answers[key])) { 56 | data[key] = {} 57 | answers[key].forEach(function (multiChoiceAnswer) { 58 | data[key][multiChoiceAnswer] = true 59 | }) 60 | } else if (typeof answers[key] === 'string') { 61 | data[key] = answers[key].replace(/"/g, '\\"') 62 | } else { 63 | data[key] = answers[key] 64 | } 65 | done() 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /lib/loaders.js: -------------------------------------------------------------------------------- 1 | // https://github.com/vuejs-templates/webpack/blob/master/template/build/utils.js 2 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 3 | 4 | exports.cssLoaders = function (options) { 5 | options = options || {} 6 | // generate loader string to be used with extract text plugin 7 | function generateLoaders (loaders) { 8 | var sourceLoader = loaders.map(function (loader) { 9 | var extraParamChar 10 | if (/\?/.test(loader)) { 11 | loader = loader.replace(/\?/, '-loader?') 12 | extraParamChar = '&' 13 | } else { 14 | loader = loader + '-loader' 15 | extraParamChar = '?' 16 | } 17 | return loader + (options.sourceMap ? extraParamChar + 'sourceMap' : '') 18 | }).join('!') 19 | 20 | // Extract CSS when that option is specified 21 | // (which is the case during production build) 22 | if (options.extract) { 23 | return ExtractTextPlugin.extract({ 24 | use: sourceLoader, 25 | fallback: 'vue-style-loader' 26 | }) 27 | } else { 28 | return ['vue-style-loader', sourceLoader].join('!') 29 | } 30 | } 31 | 32 | // http://vuejs.github.io/vue-loader/en/configurations/extract-css.html 33 | return { 34 | css: generateLoaders(['css?-autoprefixer', 'postcss']), 35 | less: generateLoaders(['css?-autoprefixer', 'postcss', 'less']), 36 | sass: generateLoaders(['css?-autoprefixer', 'postcss', 'sass?indentedSyntax']), 37 | scss: generateLoaders(['css?-autoprefixer', 'postcss', 'sass']), 38 | stylus: generateLoaders(['css?-autoprefixer', 'postcss', 'stylus']), 39 | styl: generateLoaders(['css?-autoprefixer', 'postcss', 'stylus']) 40 | } 41 | } 42 | 43 | // Generate loaders for standalone style files (outside of .vue) 44 | exports.styleLoaders = function (options) { 45 | var output = [] 46 | var loaders = exports.cssLoaders(options) 47 | for (var extension in loaders) { 48 | var loader = loaders[extension] 49 | output.push({ 50 | test: new RegExp('\\.' + extension + '$'), 51 | loader: loader 52 | }) 53 | } 54 | return output 55 | } 56 | -------------------------------------------------------------------------------- /lib/options.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var metadata = require('read-metadata') 3 | var exists = require('fs').existsSync 4 | var getGitUser = require('./git-user') 5 | var validateName = require('validate-npm-package-name') 6 | 7 | /** 8 | * Read prompts metadata. 9 | * 10 | * @param {String} dir 11 | * @return {Object} 12 | */ 13 | 14 | module.exports = function options (name, dir) { 15 | var opts = getMetadata(dir) 16 | 17 | setDefault(opts, 'name', name) 18 | setValidateName(opts) 19 | 20 | var author = getGitUser() 21 | if (author) { 22 | setDefault(opts, 'author', author) 23 | } 24 | 25 | return opts 26 | } 27 | 28 | /** 29 | * Gets the metadata from either a meta.json or meta.js file. 30 | * 31 | * @param {String} dir 32 | * @return {Object} 33 | */ 34 | 35 | function getMetadata (dir) { 36 | var json = path.join(dir, 'meta.json') 37 | var js = path.join(dir, 'meta.js') 38 | var opts = {} 39 | 40 | if (exists(json)) { 41 | opts = metadata.sync(json) 42 | } else if (exists(js)) { 43 | var req = require(path.resolve(js)) 44 | if (req !== Object(req)) { 45 | throw new Error('meta.js needs to expose an object') 46 | } 47 | opts = req 48 | } 49 | 50 | return opts 51 | } 52 | 53 | /** 54 | * Set the default value for a prompt question 55 | * 56 | * @param {Object} opts 57 | * @param {String} key 58 | * @param {String} val 59 | */ 60 | 61 | function setDefault (opts, key, val) { 62 | if (opts.schema) { 63 | opts.prompts = opts.schema 64 | delete opts.schema 65 | } 66 | var prompts = opts.prompts || (opts.prompts = {}) 67 | if (!prompts[key] || typeof prompts[key] !== 'object') { 68 | prompts[key] = { 69 | 'type': 'string', 70 | 'default': val 71 | } 72 | } else { 73 | prompts[key]['default'] = val 74 | } 75 | } 76 | 77 | function setValidateName (opts) { 78 | var name = opts.prompts.name 79 | var customValidate = name.validate 80 | name.validate = function (name) { 81 | var its = validateName(name) 82 | if (!its.validForNewPackages) { 83 | var errors = (its.errors || []).concat(its.warnings || []) 84 | return 'Sorry, ' + errors.join(' and ') + '.' 85 | } 86 | if (typeof customValidate === 'function') return customValidate(name) 87 | return true 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /test/e2e/test-build.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { expect } = require('chai') 3 | const execa = require('execa') 4 | const fs = require('fs') 5 | const rm = require('rimraf').sync 6 | 7 | describe('command:build', () => { 8 | const cli = path.join(__dirname, '../../bin/vue-build') 9 | let originalCwd = process.cwd() 10 | 11 | function setup () { 12 | process.chdir(path.join(__dirname, 'mock-vue-app')) 13 | } 14 | 15 | function teardown (done) { 16 | rm('dist') 17 | process.chdir(originalCwd) 18 | done() 19 | } 20 | 21 | describe('build an app', () => { 22 | let result 23 | let files 24 | before(done => { 25 | setup() 26 | execa(cli, ['index.js', '--prod']) 27 | .then(res => { 28 | result = res 29 | files = fs.readdirSync('dist') 30 | done() 31 | }) 32 | .catch(done) 33 | }) 34 | after(teardown) 35 | 36 | it('build with expected files', done => { 37 | expect(files.length).to.equal(5) 38 | expect(result.code).to.equal(0) 39 | done() 40 | }) 41 | 42 | it('build with default autoprefixer', done => { 43 | const cssFile = files.filter(file => file.endsWith('.css'))[0] 44 | expect(typeof cssFile).to.equal('string') 45 | const cssContent = fs.readFileSync(path.join('dist', cssFile), 'utf8') 46 | expect(cssContent).to.contain('-ms-flexbox') 47 | done() 48 | }) 49 | }) 50 | 51 | describe('build with local config', () => { 52 | let result 53 | let files 54 | before(done => { 55 | setup() 56 | execa(cli, ['App.vue', '--prod', '--config', 'config.js', '--lib']) 57 | .then(res => { 58 | result = res 59 | files = fs.readdirSync('dist') 60 | done() 61 | }) 62 | .catch(done) 63 | }) 64 | after(teardown) 65 | 66 | it('build with custom autoprefixer options', done => { 67 | const cssFile = files.filter(file => file.endsWith('.css'))[0] 68 | expect(typeof cssFile).to.equal('string') 69 | const cssContent = fs.readFileSync(path.join('dist', cssFile), 'utf8') 70 | expect(cssContent).to.not.contain('-ms-flexbox') 71 | expect(result.code).to.equal(0) 72 | done() 73 | }) 74 | 75 | it('export in umd format', done => { 76 | const m = require(path.join(process.cwd(), 'dist/App.js')) 77 | expect(typeof m.render).to.equal('function') 78 | done() 79 | }) 80 | }) 81 | }) 82 | -------------------------------------------------------------------------------- /lib/run.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var chalk = require('chalk') 3 | var rm = require('rimraf').sync 4 | var webpack = require('webpack') 5 | var logger = require('./logger') 6 | var createServer = require('../lib/server') 7 | 8 | module.exports = function (webpackConfig, options) { 9 | if (typeof options.run === 'function') { 10 | return options.run(webpackConfig, options) 11 | } 12 | 13 | try { 14 | var compiler = webpack(webpackConfig) 15 | } catch (err) { 16 | if (err.name === 'WebpackOptionsValidationError') { 17 | logger.fatal(err.message) 18 | } else { 19 | throw err 20 | } 21 | } 22 | 23 | if (options.watch) { 24 | console.log('> Running in watch mode') 25 | rm(path.join(options.dist, '*')) 26 | compiler.watch({}, (err, stats) => handleBuild(err, stats, true)) 27 | } else if (options.production) { 28 | console.log('> Creating an optimized production build:\n') 29 | // remove dist files but keep that folder in production mode 30 | rm(path.join(options.dist, '*')) 31 | compiler.run(handleBuild) 32 | } else { 33 | var server = createServer(compiler, options) 34 | 35 | server.listen(options.port, options.host) 36 | if (options.open) { 37 | require('opn')(`http://${options.host}:${options.port}`) 38 | } 39 | } 40 | 41 | function handleBuild (err, stats, watch) { 42 | if (watch) { 43 | process.stdout.write('\x1Bc') 44 | } 45 | if (err) { 46 | process.exitCode = 1 47 | return console.error(err.stack) 48 | } 49 | if (stats.hasErrors() || stats.hasWarnings()) { 50 | process.exitCode = 1 51 | return console.error(stats.toString('errors-only')) 52 | } 53 | console.log(stats.toString({ 54 | chunks: false, 55 | children: false, 56 | modules: false, 57 | colors: true 58 | })) 59 | console.log(`\n${chalk.bgGreen.black(' SUCCESS ')} Compiled successfully.\n`) 60 | if (!watch) { 61 | if (options.lib) { 62 | console.log(`The ${chalk.cyan(options.dist)} folder is ready to be published.`) 63 | console.log(`Make sure you have correctly set ${chalk.cyan('package.json')}\n`) 64 | } else { 65 | console.log(`The ${chalk.cyan(options.dist)} folder is ready to be deployed.`) 66 | console.log(`You may also serve it locally with a static server:\n`) 67 | console.log(` ${chalk.yellow('npm')} i -g serve`) 68 | console.log(` ${chalk.yellow('serve')} ${options.dist}\n`) 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-cli", 3 | "version": "2.8.2", 4 | "description": "A simple CLI for scaffolding Vue.js projects.", 5 | "preferGlobal": true, 6 | "bin": { 7 | "vue": "bin/vue", 8 | "vue-init": "bin/vue-init", 9 | "vue-list": "bin/vue-list", 10 | "vue-build": "bin/vue-build" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/vuejs/vue-cli.git" 15 | }, 16 | "keywords": [ 17 | "vue", 18 | "cli", 19 | "spa" 20 | ], 21 | "author": "Evan You", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/vuejs/vue-cli/issues" 25 | }, 26 | "homepage": "https://github.com/vuejs/vue-cli#readme", 27 | "main": "lib/index.js", 28 | "scripts": { 29 | "test": "npm run lint && npm run e2e && npm run e2e:build", 30 | "lint": "eslint test/e2e/test*.js lib bin/* --env mocha", 31 | "e2e": "rimraf test/e2e/mock-template-build && cross-env BABEL_ENV=development mocha test/e2e/test.js --slow 1000 --compilers js:babel-core/register", 32 | "e2e:build": "cross-env BABEL_ENV=development mocha test/e2e/test-build.js --timeout 60000 --compilers js:babel-core/register" 33 | }, 34 | "dependencies": { 35 | "async": "^2.0.0-rc.2", 36 | "autoprefixer": "^6.6.1", 37 | "babel-core": "^6.21.0", 38 | "babel-loader": "^6.2.10", 39 | "babel-preset-vue-app": "^0.4.0", 40 | "chalk": "^1.1.1", 41 | "commander": "^2.9.0", 42 | "connect-history-api-fallback": "^1.3.0", 43 | "consolidate": "^0.14.0", 44 | "copy-webpack-plugin": "^4.0.1", 45 | "css-loader": "^0.26.1", 46 | "download-git-repo": "^0.2.1", 47 | "express": "^4.14.0", 48 | "extract-text-webpack-plugin": "^2.0.0-rc.3", 49 | "file-loader": "^0.9.0", 50 | "friendly-errors-webpack-plugin": "^1.1.2", 51 | "handlebars": "^4.0.5", 52 | "html-webpack-plugin": "^2.26.0", 53 | "http-proxy-middleware": "^0.17.3", 54 | "inquirer": "^0.12.0", 55 | "installed-by-yarn-globally": "^0.1.1", 56 | "metalsmith": "^2.1.0", 57 | "minimatch": "^3.0.0", 58 | "multimatch": "^2.1.0", 59 | "opn": "^4.0.2", 60 | "ora": "^0.2.1", 61 | "post-compile-webpack-plugin": "^0.1.0", 62 | "postcss-loader": "^1.2.1", 63 | "read-metadata": "^1.0.0", 64 | "request": "^2.67.0", 65 | "rimraf": "^2.6.1", 66 | "semver": "^5.1.0", 67 | "tildify": "^1.2.0", 68 | "url-loader": "^0.5.7", 69 | "user-home": "^2.0.0", 70 | "validate-npm-package-name": "^2.2.2", 71 | "vue": "^2.1.10", 72 | "vue-loader": "^10.0.2", 73 | "vue-template-compiler": "^2.1.10", 74 | "webpack": "^2.2.0", 75 | "webpack-dev-middleware": "^1.9.0", 76 | "webpack-hot-middleware": "^2.15.0", 77 | "webpack-merge": "^2.3.1" 78 | }, 79 | "devDependencies": { 80 | "babel-preset-es2015": "^6.22.0", 81 | "babel-preset-stage-2": "^6.22.0", 82 | "chai": "^3.5.0", 83 | "cross-env": "^1.0.7", 84 | "eslint": "^2.7.0", 85 | "eslint-config-standard": "^5.1.0", 86 | "eslint-plugin-promise": "^1.1.0", 87 | "eslint-plugin-standard": "^1.3.2", 88 | "execa": "^0.5.0", 89 | "mocha": "^2.4.5" 90 | }, 91 | "engines": { 92 | "node": ">=4.0.0" 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /bin/vue-init: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var download = require('download-git-repo') 4 | var program = require('commander') 5 | var exists = require('fs').existsSync 6 | var path = require('path') 7 | var ora = require('ora') 8 | var home = require('user-home') 9 | var tildify = require('tildify') 10 | var chalk = require('chalk') 11 | var inquirer = require('inquirer') 12 | var rm = require('rimraf').sync 13 | var logger = require('../lib/logger') 14 | var generate = require('../lib/generate') 15 | var checkVersion = require('../lib/check-version') 16 | var warnings = require('../lib/warnings') 17 | var localPath = require('../lib/local-path') 18 | 19 | var isLocalPath = localPath.isLocalPath 20 | var getTemplatePath = localPath.getTemplatePath 21 | 22 | /** 23 | * Usage. 24 | */ 25 | 26 | program 27 | .usage(' [project-name]') 28 | .option('-c, --clone', 'use git clone') 29 | .option('--offline', 'use cached template') 30 | 31 | /** 32 | * Help. 33 | */ 34 | 35 | program.on('--help', function () { 36 | console.log(' Examples:') 37 | console.log() 38 | console.log(chalk.gray(' # create a new project with an official template')) 39 | console.log(' $ vue init webpack my-project') 40 | console.log() 41 | console.log(chalk.gray(' # create a new project straight from a github template')) 42 | console.log(' $ vue init username/repo my-project') 43 | console.log() 44 | }) 45 | 46 | /** 47 | * Help. 48 | */ 49 | 50 | function help () { 51 | program.parse(process.argv) 52 | if (program.args.length < 1) return program.help() 53 | } 54 | help() 55 | 56 | /** 57 | * Settings. 58 | */ 59 | 60 | var template = program.args[0] 61 | var hasSlash = template.indexOf('/') > -1 62 | var rawName = program.args[1] 63 | var inPlace = !rawName || rawName === '.' 64 | var name = inPlace ? path.relative('../', process.cwd()) : rawName 65 | var to = path.resolve(rawName || '.') 66 | var clone = program.clone || false 67 | 68 | var tmp = path.join(home, '.vue-templates', template.replace(/\//g, '-')) 69 | if (program.offline) { 70 | console.log(`> Use cached template at ${chalk.yellow(tildify(tmp))}`) 71 | template = tmp 72 | } 73 | 74 | /** 75 | * Padding. 76 | */ 77 | 78 | console.log() 79 | process.on('exit', function () { 80 | console.log() 81 | }) 82 | 83 | if (exists(to)) { 84 | inquirer.prompt([{ 85 | type: 'confirm', 86 | message: inPlace 87 | ? 'Generate project in current directory?' 88 | : 'Target directory exists. Continue?', 89 | name: 'ok' 90 | }], function (answers) { 91 | if (answers.ok) { 92 | run() 93 | } 94 | }) 95 | } else { 96 | run() 97 | } 98 | 99 | /** 100 | * Check, download and generate the project. 101 | */ 102 | 103 | function run () { 104 | // check if template is local 105 | if (isLocalPath(template)) { 106 | var templatePath = getTemplatePath(template) 107 | if (exists(templatePath)) { 108 | generate(name, templatePath, to, function (err) { 109 | if (err) logger.fatal(err) 110 | console.log() 111 | logger.success('Generated "%s".', name) 112 | }) 113 | } else { 114 | logger.fatal('Local template "%s" not found.', template) 115 | } 116 | } else { 117 | checkVersion(function () { 118 | if (!hasSlash) { 119 | // use official templates 120 | var officialTemplate = 'vuejs-templates/' + template 121 | if (template.indexOf('#') !== -1) { 122 | downloadAndGenerate(officialTemplate) 123 | } else { 124 | if (template.indexOf('-2.0') !== -1) { 125 | warnings.v2SuffixTemplatesDeprecated(template, inPlace ? '' : name) 126 | return 127 | } 128 | 129 | // warnings.v2BranchIsNowDefault(template, inPlace ? '' : name) 130 | downloadAndGenerate(officialTemplate) 131 | } 132 | } else { 133 | downloadAndGenerate(template) 134 | } 135 | }) 136 | } 137 | } 138 | 139 | /** 140 | * Download a generate from a template repo. 141 | * 142 | * @param {String} template 143 | */ 144 | 145 | function downloadAndGenerate (template) { 146 | var spinner = ora('downloading template') 147 | spinner.start() 148 | // Remove if local template exists 149 | if (exists(tmp)) rm(tmp) 150 | download(template, tmp, { clone: clone }, function (err) { 151 | spinner.stop() 152 | if (err) logger.fatal('Failed to download repo ' + template + ': ' + err.message.trim()) 153 | generate(name, tmp, to, function (err) { 154 | if (err) logger.fatal(err) 155 | console.log() 156 | logger.success('Generated "%s".', name) 157 | }) 158 | }) 159 | } 160 | -------------------------------------------------------------------------------- /lib/generate.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk') 2 | var Metalsmith = require('metalsmith') 3 | var Handlebars = require('handlebars') 4 | var async = require('async') 5 | var render = require('consolidate').handlebars.render 6 | var path = require('path') 7 | var multimatch = require('multimatch') 8 | var getOptions = require('./options') 9 | var ask = require('./ask') 10 | var filter = require('./filter') 11 | var logger = require('./logger') 12 | 13 | // register handlebars helper 14 | Handlebars.registerHelper('if_eq', function (a, b, opts) { 15 | return a === b 16 | ? opts.fn(this) 17 | : opts.inverse(this) 18 | }) 19 | 20 | Handlebars.registerHelper('unless_eq', function (a, b, opts) { 21 | return a === b 22 | ? opts.inverse(this) 23 | : opts.fn(this) 24 | }) 25 | 26 | /** 27 | * Generate a template given a `src` and `dest`. 28 | * 29 | * @param {String} name 30 | * @param {String} src 31 | * @param {String} dest 32 | * @param {Function} done 33 | */ 34 | 35 | module.exports = function generate (name, src, dest, done) { 36 | var opts = getOptions(name, src) 37 | var metalsmith = Metalsmith(path.join(src, 'template')) 38 | var data = Object.assign(metalsmith.metadata(), { 39 | destDirName: name, 40 | inPlace: dest === process.cwd(), 41 | noEscape: true 42 | }) 43 | opts.helpers && Object.keys(opts.helpers).map(function (key) { 44 | Handlebars.registerHelper(key, opts.helpers[key]) 45 | }) 46 | 47 | var helpers = {chalk, logger} 48 | 49 | if (opts.metalsmith && typeof opts.metalsmith.before === 'function') { 50 | opts.metalsmith.before(metalsmith, opts, helpers) 51 | } 52 | 53 | metalsmith.use(askQuestions(opts.prompts)) 54 | .use(filterFiles(opts.filters)) 55 | .use(renderTemplateFiles(opts.skipInterpolation)) 56 | 57 | if (typeof opts.metalsmith === 'function') { 58 | opts.metalsmith(metalsmith, opts, helpers) 59 | } else if (opts.metalsmith && typeof opts.metalsmith.after === 'function') { 60 | opts.metalsmith.after(metalsmith, opts, helpers) 61 | } 62 | 63 | metalsmith.clean(false) 64 | .source('.') // start from template root instead of `./src` which is Metalsmith's default for `source` 65 | .destination(dest) 66 | .build(function (err, files) { 67 | done(err) 68 | if (typeof opts.complete === 'function') { 69 | var helpers = {chalk, logger, files} 70 | opts.complete(data, helpers) 71 | } else { 72 | logMessage(opts.completeMessage, data) 73 | } 74 | }) 75 | 76 | return data 77 | } 78 | 79 | /** 80 | * Create a middleware for asking questions. 81 | * 82 | * @param {Object} prompts 83 | * @return {Function} 84 | */ 85 | 86 | function askQuestions (prompts) { 87 | return function (files, metalsmith, done) { 88 | ask(prompts, metalsmith.metadata(), done) 89 | } 90 | } 91 | 92 | /** 93 | * Create a middleware for filtering files. 94 | * 95 | * @param {Object} filters 96 | * @return {Function} 97 | */ 98 | 99 | function filterFiles (filters) { 100 | return function (files, metalsmith, done) { 101 | filter(files, filters, metalsmith.metadata(), done) 102 | } 103 | } 104 | 105 | /** 106 | * Template in place plugin. 107 | * 108 | * @param {Object} files 109 | * @param {Metalsmith} metalsmith 110 | * @param {Function} done 111 | */ 112 | 113 | function renderTemplateFiles (skipInterpolation) { 114 | skipInterpolation = typeof skipInterpolation === 'string' 115 | ? [skipInterpolation] 116 | : skipInterpolation 117 | return function (files, metalsmith, done) { 118 | var keys = Object.keys(files) 119 | var metalsmithMetadata = metalsmith.metadata() 120 | async.each(keys, function (file, next) { 121 | // skipping files with skipInterpolation option 122 | if (skipInterpolation && multimatch([file], skipInterpolation, { dot: true }).length) { 123 | return next() 124 | } 125 | var str = files[file].contents.toString() 126 | // do not attempt to render files that do not have mustaches 127 | if (!/{{([^{}]+)}}/g.test(str)) { 128 | return next() 129 | } 130 | render(str, metalsmithMetadata, function (err, res) { 131 | if (err) { 132 | err.message = `[${file}] ${err.message}` 133 | return next(err) 134 | } 135 | files[file].contents = new Buffer(res) 136 | next() 137 | }) 138 | }, done) 139 | } 140 | } 141 | 142 | /** 143 | * Display template complete message. 144 | * 145 | * @param {String} message 146 | * @param {Object} data 147 | */ 148 | 149 | function logMessage (message, data) { 150 | if (!message) return 151 | render(message, data, function (err, res) { 152 | if (err) { 153 | console.error('\n Error when rendering template complete message: ' + err.message.trim()) 154 | } else { 155 | console.log('\n' + res.split(/\r?\n/g).map(function (line) { 156 | return ' ' + line 157 | }).join('\n')) 158 | } 159 | }) 160 | } 161 | -------------------------------------------------------------------------------- /docs/build.md: -------------------------------------------------------------------------------- 1 | # vue build 2 | 3 | `vue build` command gives you a zero-configuration development setup, install once and build everywhere. 4 | 5 | ## Features 6 | 7 | - **Not a boilerplate**: run a single command to develop your app 8 | - **Out of the box**: ES2015, single-file component with hot reloading and custom CSS preprocessors 9 | - **Customizable**: populate a `~/.vue/webpack.config.js` for custom webpack config 10 | - **Single-file component mode**: simply run `vue build Component.vue` and test it out in the browser! 11 | 12 | ## Get started 13 | 14 | Make sure that you've installed `vue-cli` with `npm >= 3` or `yarn >= 0.7`. 15 | 16 | Populate an app entry `./index.js` in your project: 17 | 18 | ```js 19 | import Vue from 'vue' 20 | 21 | new Vue({ 22 | el: '#app', 23 | render: h => h('h2', 'hello world') 24 | }) 25 | ``` 26 | 27 | And then run `vue build index.js` and go to `http://localhost:4000` 28 | 29 | **To build for production (minimized and optimized):** 30 | 31 | ```bash 32 | $ vue build index.js --prod 33 | ``` 34 | 35 | If you want to directly test a component without manually create a Vue instance for it, try: 36 | 37 | ```bash 38 | $ vue build Component.vue 39 | ``` 40 | 41 |
How does this work?
42 | When the input file ends with `.vue` extension, we use a [default app entry](/lib/default-entry.es6) to load the given component, otherwise we treat it as a normal webpack entry. For jsx component which ends with `.js` extension, you can enable this behavior manually by adding `--mount`. 43 |
44 | 45 | **To distribute component:** 46 | 47 | ```bash 48 | $ vue build Component.vue --prod --lib 49 | ``` 50 | 51 | This will create an optimized bundle in UMD format, and the name of exported library is set to `Component`, you can use `--lib [CustomLibraryName]` to customize it. 52 | 53 | Note that in some cases you may use [`externals`](https://webpack.js.org/configuration/externals/) to exclude some modules from your bundle. 54 | 55 | **Watch mode:** 56 | 57 | ```bash 58 | $ vue build index.js --watch 59 | ``` 60 | 61 | It's similar to `development mode` but does not add hot-reloading support and uses a real file system. 62 | 63 | **For more CLI usages:** 64 | 65 | ```bash 66 | $ vue build -h 67 | ``` 68 | 69 | ## Configuration files 70 | 71 | By default, we use `~/.vue/config.js` and `~/.vue/webpack.config.js` if they exist. 72 | 73 | To use a custom config file, add `--config [file]` 74 | 75 | To use a custom webpack config file, add `--webpack [file]` 76 | 77 | ### config.js 78 | 79 | You can define CLI options in this file. 80 | 81 | #### entry 82 | 83 | Type: `string` `Array` `Object` 84 | 85 | It's the first argument of `vue build` command, eg: `vue build entry.js`. You can set it here to omit it in CLI arguments. 86 | 87 | The single-component mode (`--mount`) will not work if you set `entry` to an `Array` or `Object`. 88 | 89 | - `Array`: Override `webpackConfig.entry.client` 90 | - `Object`: Override `webpackConfig.entry` 91 | - `string`: Added to `webpackConfig.entry.client` or used as `webpackConfig.resolve.alias['your-tasteful-component']` in single-component mode. 92 | 93 | #### port 94 | 95 | Type: `number`
96 | Default: `4000` 97 | 98 | Port of dev server. 99 | 100 | #### webpack 101 | 102 | Type: `function` `string` `object` 103 | 104 | ##### function 105 | 106 | `webpack(webpackConfig, options, webpack)` 107 | 108 | - webpackConfig: current webpack config 109 | - options: CLI options (assigned with config.js) 110 | - webpack: The `webpack` module 111 | 112 | Return a new webpack config. 113 | 114 | ##### string 115 | 116 | Used as the path to webpack config file, eg: `--webpack webpack.config.js` 117 | 118 | ##### object 119 | 120 | Directly use as webpack config. 121 | 122 | Note that we use [webpack-merge](https://github.com/survivejs/webpack-merge) to merge your webpack config with default webpack config. 123 | 124 | #### autoprefixer 125 | 126 | Type: `object` 127 | 128 | Autoprefixer options, default value: 129 | 130 | ```js 131 | { 132 | browsers: ['ie > 8', 'last 5 versions'] 133 | } 134 | ``` 135 | 136 | #### postcss 137 | 138 | Type: `Object` `Array` `Function` 139 | 140 | PostCSS options, if it's an `Array` or `Function`, the default value will be override: 141 | 142 | ```js 143 | { 144 | plugins: [ 145 | require('autoprefixer')(options.autoprefixer) 146 | ] 147 | } 148 | ``` 149 | 150 | #### babel 151 | 152 | Type: `Object` 153 | 154 | [Babel options](https://github.com/babel/babel-loader#options). You can set `babel.babelrc` to false to disable using `.babelrc`. 155 | 156 | #### html 157 | 158 | Type: `Object` `Array` `boolean` 159 | 160 | [html-webpack-plugin](https://github.com/ampedandwired/html-webpack-plugin) options, use this option to customize `index.html` output, default value: 161 | 162 | ```js 163 | { 164 | title: 'Vue App', 165 | template: path.join(__dirname, '../lib/template.html') 166 | } 167 | ``` 168 | 169 | Check out the [default template](/lib/template.html) file we use. To disable generating html file, you can set `html` to `false`. 170 | 171 | #### filename 172 | 173 | Set custom filename for `js` `css` `static` files: 174 | 175 | ```js 176 | { 177 | filename: { 178 | js: 'index.js', 179 | css: 'style.css', 180 | static: 'static/[name].[ext]' 181 | } 182 | } 183 | ``` 184 | 185 | #### disableCompress 186 | 187 | Type: `boolean` 188 | 189 | In production mode, all generated files will be compressed and produce sourcemaps file. You can use `--disableCompress` to disable this behavior. 190 | 191 | #### hmrEntries 192 | 193 | Type: `Array`
194 | Default: `['client']` 195 | 196 | Add `webpack-hot-middleware` HMR client to specific webpack entries. By default your app is loaded in `client` entry, so we insert it here. 197 | 198 | #### proxy 199 | 200 | Type: `string`, `Object` 201 | 202 | To tell the development server to serve any `/api/*` request to your API server in development, use the `proxy` options: 203 | 204 | ```js 205 | module.exports = { 206 | proxy: 'http://localhost:8080/api' 207 | } 208 | ``` 209 | 210 | This way, when you fetch `/api/todos` in your Vue app, the development server will proxy your request to `http://localhost:8080/api/todos`. 211 | 212 | We use [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) under the hood, so the `proxy` option can also be an object: 213 | 214 | ```js 215 | module.exports = { 216 | proxy: { 217 | '/api/foo': 'http://localhost:8080/api', 218 | '/api/fake-data': { 219 | target: 'http://jsonplaceholder.typicode.com', 220 | changeOrigin: true, 221 | pathRewrite: { 222 | '^/api/fake-data': '' 223 | } 224 | } 225 | } 226 | } 227 | ``` 228 | 229 | Keep in mind that proxy only has effect in development. 230 | 231 | #### setup 232 | 233 | Type: `function` 234 | 235 | Perform some custom logic to development server: 236 | 237 | ```js 238 | module.exports = { 239 | setup(app) { 240 | app.get('/api', (req, res) => { 241 | res.end('This is the API') 242 | }) 243 | } 244 | } 245 | ``` 246 | 247 | #### run(webpackConfig, options) 248 | 249 | Type: `function` 250 | 251 | You can use a custom `run` function to perform your own build process instead of the default one. For example, run karma with the processed webpack config: 252 | 253 | ```js 254 | const Server = require('karma').Server 255 | 256 | module.exports = { 257 | run(webpackConfig) { 258 | const server = new Server({ 259 | webpack: webpackConfig, 260 | // other karma options... 261 | }, exitCode => { 262 | console.log('Karma has exited with ' + exitCode) 263 | process.exit(exitCode) 264 | }) 265 | server.start() 266 | } 267 | } 268 | ``` 269 | 270 | ### webpack.config.js 271 | 272 | All the webpack options are available here. 273 | 274 | ## Recipes 275 | 276 | ### Custom CSS preprocessors 277 | 278 | CSS preprocessors (and CSS extraction) work out of the box, install relevant loaders and you're all set! For example, add `sass` support: 279 | 280 | ```bash 281 | $ npm i -D node-sass sass-loader 282 | ``` 283 | 284 | Since all CSS will be piped through `postcss-loader`, `autoprefixer` and `postcss` options will always work no matter what CSS preprocessors you're using. 285 | 286 | ### Custom babel config 287 | 288 | By default we only use a single babel preset: [babel-preset-vue-app](https://github.com/egoist/babel-preset-vue-app) which includes following features: 289 | 290 | - ES2015/2016/2017 and Stage-2 features 291 | - Transform `async/await` and `generator` 292 | - Transform Vue JSX 293 | 294 | You can set `babel` option in config file or populate a `.babelrc` in project root directory to override it. 295 | 296 | ### Copy static files 297 | 298 | Everything in `./static` folder will be copied to dist folder, for example: `static/favicon.ico` will be copied to `dist/favicon.ico`. 299 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-cli [![Build Status](https://img.shields.io/circleci/project/vuejs/vue-cli/master.svg)](https://circleci.com/gh/vuejs/vue-cli) [![npm package](https://img.shields.io/npm/v/vue-cli.svg)](https://www.npmjs.com/package/vue-cli) 2 | 3 | A simple CLI for scaffolding Vue.js projects. 4 | 5 | ### Installation 6 | 7 | Prerequisites: [Node.js](https://nodejs.org/en/) (>=4.x, 6.x preferred), npm version 3+ and [Git](https://git-scm.com/). 8 | 9 | ``` bash 10 | $ npm install -g vue-cli 11 | ``` 12 | 13 | ### Usage 14 | 15 | ``` bash 16 | $ vue init 17 | ``` 18 | 19 | Example: 20 | 21 | ``` bash 22 | $ vue init webpack my-project 23 | ``` 24 | 25 | The above command pulls the template from [vuejs-templates/webpack](https://github.com/vuejs-templates/webpack), prompts for some information, and generates the project at `./my-project/`. 26 | 27 | ### vue build 28 | 29 | Use vue-cli as a zero-configuration development tool for your Vue apps and component, check out the [docs](/docs/build.md). 30 | 31 | ### Official Templates 32 | 33 | The purpose of official Vue project templates are to provide opinionated, battery-included development tooling setups so that users can get started with actual app code as fast as possible. However, these templates are un-opinionated in terms of how you structure your app code and what libraries you use in addition to Vue.js. 34 | 35 | All official project templates are repos in the [vuejs-templates organization](https://github.com/vuejs-templates). When a new template is added to the organization, you will be able to run `vue init ` to use that template. You can also run `vue list` to see all available official templates. 36 | 37 | Current available templates include: 38 | 39 | - [webpack](https://github.com/vuejs-templates/webpack) - A full-featured Webpack + vue-loader setup with hot reload, linting, testing & css extraction. 40 | 41 | - [webpack-simple](https://github.com/vuejs-templates/webpack-simple) - A simple Webpack + vue-loader setup for quick prototyping. 42 | 43 | - [browserify](https://github.com/vuejs-templates/browserify) - A full-featured Browserify + vueify setup with hot-reload, linting & unit testing. 44 | 45 | - [browserify-simple](https://github.com/vuejs-templates/browserify-simple) - A simple Browserify + vueify setup for quick prototyping. 46 | 47 | - [pwa](https://github.com/vuejs-templates/pwa) - PWA template for vue-cli based on the webpack template 48 | 49 | - [simple](https://github.com/vuejs-templates/simple) - The simplest possible Vue setup in a single HTML file 50 | 51 | ### Custom Templates 52 | 53 | It's unlikely to make everyone happy with the official templates. You can simply fork an official template and then use it via `vue-cli` with: 54 | 55 | ``` bash 56 | vue init username/repo my-project 57 | ``` 58 | 59 | Where `username/repo` is the GitHub repo shorthand for your fork. 60 | 61 | The shorthand repo notation is passed to [download-git-repo](https://github.com/flipxfx/download-git-repo) so you can also use things like `bitbucket:username/repo` for a Bitbucket repo and `username/repo#branch` for tags or branches. 62 | 63 | If you would like to download from a private repository use the `--clone` flag and the cli will use `git clone` so your SSH keys are used. 64 | 65 | ### Local Templates 66 | 67 | Instead of a GitHub repo, you can also use a template on your local file system: 68 | 69 | ``` bash 70 | vue init ~/fs/path/to-custom-template my-project 71 | ``` 72 | 73 | ### Writing Custom Templates from Scratch 74 | 75 | - A template repo **must** have a `template` directory that holds the template files. 76 | 77 | - A template repo **may** have a metadata file for the template which can be either a `meta.js` or `meta.json` file. It can contain the following fields: 78 | 79 | - `prompts`: used to collect user options data; 80 | 81 | - `filters`: used to conditional filter files to render. 82 | 83 | - `metalsmith`: used to add custom metalsmith plugins in the chain. 84 | 85 | - `completeMessage`: the message to be displayed to the user when the template has been generated. You can include custom instruction here. 86 | 87 | - `complete`: Instead of using `completeMessage`, you can use a function to run stuffs when the template has been generated. 88 | 89 | #### prompts 90 | 91 | The `prompts` field in the metadata file should be an object hash containing prompts for the user. For each entry, the key is the variable name and the value is an [Inquirer.js question object](https://github.com/SBoudrias/Inquirer.js/#question). Example: 92 | 93 | ``` json 94 | { 95 | "prompts": { 96 | "name": { 97 | "type": "string", 98 | "required": true, 99 | "message": "Project name" 100 | } 101 | } 102 | } 103 | ``` 104 | 105 | After all prompts are finished, all files inside `template` will be rendered using [Handlebars](http://handlebarsjs.com/), with the prompt results as the data. 106 | 107 | ##### Conditional Prompts 108 | 109 | A prompt can be made conditional by adding a `when` field, which should be a JavaScript expression evaluated with data collected from previous prompts. For example: 110 | 111 | ``` json 112 | { 113 | "prompts": { 114 | "lint": { 115 | "type": "confirm", 116 | "message": "Use a linter?" 117 | }, 118 | "lintConfig": { 119 | "when": "lint", 120 | "type": "list", 121 | "message": "Pick a lint config", 122 | "choices": [ 123 | "standard", 124 | "airbnb", 125 | "none" 126 | ] 127 | } 128 | } 129 | } 130 | ``` 131 | 132 | The prompt for `lintConfig` will only be triggered when the user answered yes to the `lint` prompt. 133 | 134 | ##### Pre-registered Handlebars Helpers 135 | 136 | Two commonly used Handlebars helpers, `if_eq` and `unless_eq` are pre-registered: 137 | 138 | ``` handlebars 139 | {{#if_eq lintConfig "airbnb"}};{{/if_eq}} 140 | ``` 141 | 142 | ##### Custom Handlebars Helpers 143 | 144 | You may want to register additional Handlebars helpers using the `helpers` property in the metadata file. The object key is the helper name: 145 | 146 | ``` js 147 | module.exports = { 148 | helpers: { 149 | lowercase: str => str.toLowerCase() 150 | } 151 | } 152 | ``` 153 | 154 | Upon registration, they can be used as follows: 155 | 156 | ``` handlebars 157 | {{ lowercase name }} 158 | ``` 159 | 160 | #### File filters 161 | 162 | The `filters` field in the metadata file should be an object hash containing file filtering rules. For each entry, the key is a [minimatch glob pattern](https://github.com/isaacs/minimatch) and the value is a JavaScript expression evaluated in the context of prompt answers data. Example: 163 | 164 | ``` json 165 | { 166 | "filters": { 167 | "test/**/*": "needTests" 168 | } 169 | } 170 | ``` 171 | 172 | Files under `test` will only be generated if the user answered yes to the prompt for `needTests`. 173 | 174 | Note that the `dot` option for minimatch is set to `true` so glob patterns would also match dotfiles by default. 175 | 176 | #### Skip rendering 177 | 178 | The `skipInterpolation` field in the metadata file should be a [minimatch glob pattern](https://github.com/isaacs/minimatch). The files matched should skip rendering. Example: 179 | 180 | ``` json 181 | { 182 | "skipInterpolation": "src/**/*.vue" 183 | } 184 | ``` 185 | 186 | #### Metalsmith 187 | 188 | `vue-cli` uses [metalsmith](https://github.com/segmentio/metalsmith) to generate the project. 189 | 190 | You may customize the metalsmith builder created by vue-cli to register custom plugins. 191 | 192 | ```js 193 | { 194 | "metalsmith": function (metalsmith, opts, helpers) { 195 | function customMetalsmithPlugin (files, metalsmith, done) { 196 | // Implement something really custom here. 197 | done(null, files) 198 | } 199 | 200 | metalsmith.use(customMetalsmithPlugin) 201 | } 202 | } 203 | ``` 204 | 205 | If you need to hook metalsmith before questions are asked, you may use an object with `before` key. 206 | 207 | ```js 208 | { 209 | "metalsmith": { 210 | before: function (metalsmith, opts, helpers) {}, 211 | after: function (metalsmith, opts, helpers) {} 212 | } 213 | } 214 | ``` 215 | 216 | #### Additional data available in meta.{js,json} 217 | 218 | - `destDirName` - destination directory name 219 | 220 | ```json 221 | { 222 | "completeMessage": "To get started:\n\n cd {{destDirName}}\n npm install\n npm run dev" 223 | } 224 | ``` 225 | 226 | - `inPlace` - generating template into current directory 227 | 228 | ```json 229 | { 230 | "completeMessage": "{{#inPlace}}To get started:\n\n npm install\n npm run dev.{{else}}To get started:\n\n cd {{destDirName}}\n npm install\n npm run dev.{{/inPlace}}" 231 | } 232 | ``` 233 | 234 | ### `complete` function 235 | 236 | Arguments: 237 | 238 | - `data`: the same data you can access in `completeMessage`: 239 | ```js 240 | { 241 | complete (data) { 242 | if (!data.inPlace) { 243 | console.log(`cd ${data.destDirName}`) 244 | } 245 | } 246 | } 247 | ``` 248 | 249 | - `helpers`: some helpers you can use to log results. 250 | - `chalk`: the `chalk` module 251 | - `logger`: [the built-in vue-cli logger](/lib/logger.js) 252 | - `files`: An array of generated files 253 | ```js 254 | { 255 | complete (data, {logger, chalk}) { 256 | if (!data.inPlace) { 257 | logger.log(`cd ${chalk.yellow(data.destDirName)}`) 258 | } 259 | } 260 | } 261 | ``` 262 | 263 | ### Installing a specific template version 264 | 265 | `vue-cli` uses the tool [`download-git-repo`](https://github.com/flipxfx/download-git-repo) to download the official templates used. The `download-git-repo` tool allows you to indicate a specific branch for a given repository by providing the desired branch name after a pound sign (`#`). 266 | 267 | The format needed for a specific official template is: 268 | 269 | ``` 270 | vue init '#' 271 | ``` 272 | 273 | Example: 274 | 275 | Installing the [`1.0` branch](https://github.com/vuejs-templates/webpack-simple/tree/1.0) of the webpack-simple vue template: 276 | 277 | ``` 278 | vue init 'webpack-simple#1.0' mynewproject 279 | ``` 280 | 281 | _Note_: The surrounding quotes are necessary on zsh shells because of the special meaning of the `#` character. 282 | 283 | 284 | ### License 285 | 286 | [MIT](http://opensource.org/licenses/MIT) 287 | -------------------------------------------------------------------------------- /test/e2e/test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const fs = require('fs') 3 | const path = require('path') 4 | const rm = require('rimraf').sync 5 | const exists = require('fs').existsSync 6 | const crypto = require('crypto') 7 | const render = require('consolidate').handlebars.render 8 | const inquirer = require('inquirer') 9 | const async = require('async') 10 | const extend = Object.assign || require('util')._extend 11 | const generate = require('../../lib/generate') 12 | const metadata = require('../../lib/options') 13 | const { isLocalPath, getTemplatePath } = require('../../lib/local-path') 14 | 15 | const MOCK_META_JSON_PATH = path.resolve('./test/e2e/mock-meta-json') 16 | const MOCK_METALSMITH_CUSTOM_PATH = path.resolve('./test/e2e/mock-metalsmith-custom') 17 | const MOCK_METALSMITH_CUSTOM_BEFORE_AFTER_PATH = path.resolve('./test/e2e/mock-metalsmith-custom-before-after') 18 | const MOCK_TEMPLATE_REPO_PATH = path.resolve('./test/e2e/mock-template-repo') 19 | const MOCK_TEMPLATE_BUILD_PATH = path.resolve('./test/e2e/mock-template-build') 20 | const MOCK_METADATA_REPO_JS_PATH = path.resolve('./test/e2e/mock-metadata-repo-js') 21 | const MOCK_SKIP_GLOB = path.resolve('./test/e2e/mock-skip-glob') 22 | const MOCK_ERROR = path.resolve('./test/e2e/mock-error') 23 | 24 | function monkeyPatchInquirer (answers) { 25 | // monkey patch inquirer 26 | inquirer.prompt = (questions, cb) => { 27 | const key = questions[0].name 28 | const _answers = {} 29 | const validate = questions[0].validate 30 | const valid = validate(answers[key]) 31 | if (valid !== true) { 32 | throw new Error(valid) 33 | } 34 | _answers[key] = answers[key] 35 | cb(_answers) 36 | } 37 | } 38 | 39 | describe('vue-cli', () => { 40 | const escapedAnswers = { 41 | name: 'vue-cli-test', 42 | author: 'John "The Tester" Doe ', 43 | description: 'vue-cli e2e test', 44 | preprocessor: { 45 | less: true, 46 | sass: true 47 | }, 48 | pick: 'no', 49 | noEscape: true 50 | 51 | } 52 | 53 | const answers = { 54 | name: 'vue-cli-test', 55 | author: 'John Doe ', 56 | description: 'vue-cli e2e test', 57 | preprocessor: { 58 | less: true, 59 | sass: true 60 | }, 61 | pick: 'no', 62 | noEscape: true 63 | } 64 | 65 | it('read metadata from json', () => { 66 | const meta = metadata('test-pkg', MOCK_TEMPLATE_REPO_PATH) 67 | expect(meta).to.be.an('object') 68 | expect(meta.prompts).to.have.property('description') 69 | }) 70 | 71 | it('read metadata from js', () => { 72 | const meta = metadata('test-pkg', MOCK_METADATA_REPO_JS_PATH) 73 | expect(meta).to.be.an('object') 74 | expect(meta.prompts).to.have.property('description') 75 | }) 76 | 77 | it('helpers', done => { 78 | monkeyPatchInquirer(answers) 79 | generate('test', MOCK_METADATA_REPO_JS_PATH, MOCK_TEMPLATE_BUILD_PATH, err => { 80 | if (err) done(err) 81 | const contents = fs.readFileSync(`${MOCK_TEMPLATE_BUILD_PATH}/readme.md`, 'utf-8') 82 | expect(contents).to.equal(answers.name.toUpperCase()) 83 | done() 84 | }) 85 | }) 86 | 87 | it('adds additional data to meta data', done => { 88 | const data = generate('test', MOCK_META_JSON_PATH, MOCK_TEMPLATE_BUILD_PATH, done) 89 | expect(data.destDirName).to.equal('test') 90 | expect(data.inPlace).to.equal(false) 91 | }) 92 | 93 | it('sets `inPlace` to true when generating in same directory', done => { 94 | const currentDir = process.cwd() 95 | process.chdir(MOCK_TEMPLATE_BUILD_PATH) 96 | const data = generate('test', MOCK_META_JSON_PATH, MOCK_TEMPLATE_BUILD_PATH, done) 97 | expect(data.destDirName).to.equal('test') 98 | expect(data.inPlace).to.equal(true) 99 | process.chdir(currentDir) 100 | }) 101 | 102 | it('template generation', done => { 103 | monkeyPatchInquirer(answers) 104 | generate('test', MOCK_TEMPLATE_REPO_PATH, MOCK_TEMPLATE_BUILD_PATH, err => { 105 | if (err) done(err) 106 | 107 | expect(exists(`${MOCK_TEMPLATE_BUILD_PATH}/src/yes.vue`)).to.equal(true) 108 | expect(exists(`${MOCK_TEMPLATE_BUILD_PATH}/src/no.js`)).to.equal(false) 109 | 110 | async.eachSeries([ 111 | 'package.json', 112 | 'src/yes.vue' 113 | ], function (file, next) { 114 | const template = fs.readFileSync(`${MOCK_TEMPLATE_REPO_PATH}/template/${file}`, 'utf8') 115 | const generated = fs.readFileSync(`${MOCK_TEMPLATE_BUILD_PATH}/${file}`, 'utf8') 116 | render(template, answers, (err, res) => { 117 | if (err) return next(err) 118 | expect(res).to.equal(generated) 119 | next() 120 | }) 121 | }, done) 122 | }) 123 | }) 124 | 125 | it('supports custom metalsmith plugins', done => { 126 | generate('test', MOCK_METALSMITH_CUSTOM_PATH, MOCK_TEMPLATE_BUILD_PATH, err => { 127 | if (err) done(err) 128 | 129 | expect(exists(`${MOCK_TEMPLATE_BUILD_PATH}/custom/readme.md`)).to.equal(true) 130 | 131 | async.eachSeries([ 132 | 'readme.md' 133 | ], function (file, next) { 134 | const template = fs.readFileSync(`${MOCK_METALSMITH_CUSTOM_PATH}/template/${file}`, 'utf8') 135 | const generated = fs.readFileSync(`${MOCK_TEMPLATE_BUILD_PATH}/custom/${file}`, 'utf8') 136 | render(template, {custom: 'Custom'}, (err, res) => { 137 | if (err) return next(err) 138 | expect(res).to.equal(generated) 139 | next() 140 | }) 141 | }, done) 142 | }) 143 | }) 144 | 145 | it('supports custom metalsmith plugins with after/before object keys', done => { 146 | generate('test', MOCK_METALSMITH_CUSTOM_BEFORE_AFTER_PATH, MOCK_TEMPLATE_BUILD_PATH, err => { 147 | if (err) done(err) 148 | 149 | expect(exists(`${MOCK_TEMPLATE_BUILD_PATH}/custom-before-after/readme.md`)).to.equal(true) 150 | 151 | async.eachSeries([ 152 | 'readme.md' 153 | ], function (file, next) { 154 | const template = fs.readFileSync(`${MOCK_METALSMITH_CUSTOM_BEFORE_AFTER_PATH}/template/${file}`, 'utf8') 155 | const generated = fs.readFileSync(`${MOCK_TEMPLATE_BUILD_PATH}/custom-before-after/${file}`, 'utf8') 156 | render(template, {before: 'Before', after: 'After'}, (err, res) => { 157 | if (err) return next(err) 158 | expect(res).to.equal(generated) 159 | next() 160 | }) 161 | }, done) 162 | }) 163 | }) 164 | 165 | it('generate a vaild package.json with escaped author', done => { 166 | monkeyPatchInquirer(escapedAnswers) 167 | generate('test', MOCK_TEMPLATE_REPO_PATH, MOCK_TEMPLATE_BUILD_PATH, err => { 168 | if (err) done(err) 169 | 170 | const pkg = fs.readFileSync(`${MOCK_TEMPLATE_BUILD_PATH}/package.json`, 'utf8') 171 | try { 172 | var validData = JSON.parse(pkg) 173 | expect(validData.author).to.equal(escapedAnswers.author) 174 | done() 175 | } catch (err) { 176 | done(err) 177 | } 178 | }) 179 | }) 180 | 181 | it('avoid rendering files that do not have mustaches', done => { 182 | monkeyPatchInquirer(answers) 183 | const binFilePath = `${MOCK_TEMPLATE_REPO_PATH}/template/bin.file` 184 | const wstream = fs.createWriteStream(binFilePath) 185 | wstream.write(crypto.randomBytes(100)) 186 | wstream.end() 187 | 188 | generate('test', MOCK_TEMPLATE_REPO_PATH, MOCK_TEMPLATE_BUILD_PATH, err => { 189 | if (err) done(err) 190 | 191 | const handlebarsPackageJsonFile = fs.readFileSync(`${MOCK_TEMPLATE_REPO_PATH}/template/package.json`, 'utf8') 192 | const generatedPackageJsonFile = fs.readFileSync(`${MOCK_TEMPLATE_BUILD_PATH}/package.json`, 'utf8') 193 | 194 | render(handlebarsPackageJsonFile, answers, (err, res) => { 195 | if (err) return done(err) 196 | 197 | expect(res).to.equal(generatedPackageJsonFile) 198 | expect(exists(binFilePath)).to.equal(true) 199 | expect(exists(`${MOCK_TEMPLATE_BUILD_PATH}/bin.file`)).to.equal(true) 200 | rm(binFilePath) 201 | 202 | done() 203 | }) 204 | }) 205 | }) 206 | 207 | it('avoid rendering files that match skipInterpolation option', done => { 208 | monkeyPatchInquirer(answers) 209 | const binFilePath = `${MOCK_TEMPLATE_REPO_PATH}/template/bin.file` 210 | const wstream = fs.createWriteStream(binFilePath) 211 | wstream.write(crypto.randomBytes(100)) 212 | wstream.end() 213 | 214 | generate('test', MOCK_TEMPLATE_REPO_PATH, MOCK_TEMPLATE_BUILD_PATH, err => { 215 | if (err) done(err) 216 | 217 | const originalVueFileOne = fs.readFileSync(`${MOCK_TEMPLATE_REPO_PATH}/template/src/skip-one.vue`, 'utf8') 218 | const originalVueFileTwo = fs.readFileSync(`${MOCK_TEMPLATE_REPO_PATH}/template/src/skip-two.vue`, 'utf8') 219 | const generatedVueFileOne = fs.readFileSync(`${MOCK_TEMPLATE_BUILD_PATH}/src/skip-one.vue`, 'utf8') 220 | const generatedVueFileTwo = fs.readFileSync(`${MOCK_TEMPLATE_BUILD_PATH}/src/skip-two.vue`, 'utf8') 221 | 222 | expect(originalVueFileOne).to.equal(generatedVueFileOne) 223 | expect(originalVueFileTwo).to.equal(generatedVueFileTwo) 224 | expect(exists(binFilePath)).to.equal(true) 225 | expect(exists(`${MOCK_TEMPLATE_BUILD_PATH}/bin.file`)).to.equal(true) 226 | rm(binFilePath) 227 | 228 | done() 229 | }) 230 | }) 231 | 232 | it('support multiple globs in skipInterpolation', done => { 233 | monkeyPatchInquirer(answers) 234 | const binFilePath = `${MOCK_SKIP_GLOB}/template/bin.file` 235 | const wstream = fs.createWriteStream(binFilePath) 236 | wstream.write(crypto.randomBytes(100)) 237 | wstream.end() 238 | 239 | generate('test', MOCK_SKIP_GLOB, MOCK_TEMPLATE_BUILD_PATH, err => { 240 | if (err) done(err) 241 | 242 | const originalVueFileOne = fs.readFileSync(`${MOCK_SKIP_GLOB}/template/src/no.vue`, 'utf8') 243 | const originalVueFileTwo = fs.readFileSync(`${MOCK_SKIP_GLOB}/template/src/no.js`, 'utf8') 244 | const generatedVueFileOne = fs.readFileSync(`${MOCK_TEMPLATE_BUILD_PATH}/src/no.vue`, 'utf8') 245 | const generatedVueFileTwo = fs.readFileSync(`${MOCK_TEMPLATE_BUILD_PATH}/src/no.js`, 'utf8') 246 | 247 | expect(originalVueFileOne).to.equal(generatedVueFileOne) 248 | expect(originalVueFileTwo).to.equal(generatedVueFileTwo) 249 | expect(exists(binFilePath)).to.equal(true) 250 | expect(exists(`${MOCK_TEMPLATE_BUILD_PATH}/bin.file`)).to.equal(true) 251 | rm(binFilePath) 252 | 253 | done() 254 | }) 255 | }) 256 | 257 | it('validate input value', done => { 258 | // deep copy 259 | var invalidName = extend({}, answers, { name: 'INVALID-NAME' }) 260 | monkeyPatchInquirer(invalidName) 261 | generate('INVALID-NAME', MOCK_TEMPLATE_REPO_PATH, MOCK_TEMPLATE_BUILD_PATH, err => { 262 | expect(err).to.be.an('error') 263 | done() 264 | }) 265 | }) 266 | 267 | it('custom validate', done => { 268 | var invalidName = extend({}, answers, { name: 'custom' }) 269 | monkeyPatchInquirer(invalidName) 270 | generate('test', MOCK_METADATA_REPO_JS_PATH, MOCK_TEMPLATE_BUILD_PATH, err => { 271 | expect(err).to.be.an('error') 272 | done() 273 | }) 274 | }) 275 | 276 | it('checks for local path', () => { 277 | expect(isLocalPath('../')).to.equal(true) 278 | expect(isLocalPath('../../')).to.equal(true) 279 | expect(isLocalPath('../template')).to.equal(true) 280 | expect(isLocalPath('../template/abc')).to.equal(true) 281 | expect(isLocalPath('./')).to.equal(true) 282 | expect(isLocalPath('.')).to.equal(true) 283 | expect(isLocalPath('c:/')).to.equal(true) 284 | expect(isLocalPath('D:/')).to.equal(true) 285 | 286 | expect(isLocalPath('webpack')).to.equal(false) 287 | expect(isLocalPath('username/rep')).to.equal(false) 288 | expect(isLocalPath('bitbucket:username/rep')).to.equal(false) 289 | }) 290 | 291 | it('normalizes template path', () => { 292 | expect(getTemplatePath('/')).to.equal('/') 293 | expect(getTemplatePath('/absolute/path')).to.equal('/absolute/path') 294 | 295 | expect(getTemplatePath('..')).to.equal(path.join(__dirname, '/../../..')) 296 | expect(getTemplatePath('../template')).to.equal(path.join(__dirname, '/../../../template')) 297 | }) 298 | 299 | it('points out the file in the error', done => { 300 | monkeyPatchInquirer(answers) 301 | generate('test', MOCK_ERROR, MOCK_TEMPLATE_BUILD_PATH, err => { 302 | expect(err.message).to.match(/^\[readme\.md\] Parse error/) 303 | done() 304 | }) 305 | }) 306 | }) 307 | -------------------------------------------------------------------------------- /bin/vue-build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var fs = require('fs') 4 | var path = require('path') 5 | var program = require('commander') 6 | var chalk = require('chalk') 7 | var home = require('user-home') 8 | var webpack = require('webpack') 9 | var webpackMerge = require('webpack-merge') 10 | var HtmlWebpackPlugin = require('html-webpack-plugin') 11 | var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 12 | var PostCompilePlugin = require('post-compile-webpack-plugin') 13 | var ProgressPlugin = require('webpack/lib/ProgressPlugin') 14 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 15 | var isYarn = require('installed-by-yarn-globally') 16 | var CopyPlugin = require('copy-webpack-plugin') 17 | var tildify = require('tildify') 18 | var loaders = require('../lib/loaders') 19 | var logger = require('../lib/logger') 20 | var run = require('../lib/run') 21 | 22 | /** 23 | * Usage. 24 | */ 25 | 26 | program 27 | .usage('[entry]') 28 | .option('--dist [directory]', 'Output directory for bundled files') 29 | .option('--port [port]', 'Server port') 30 | .option('--host [host]', 'Server host') 31 | .option('--prod, --production', 'Build for production') 32 | .option('-w, --watch', 'Run in watch mode') 33 | .option('-m, --mount', 'Auto-create app instance from given component, true for `.vue` file') 34 | .option('-c, --config [file]', 'Use custom config file') 35 | .option('--wp, --webpack [file]', 'Use custom webpack config file') 36 | .option('--disable-config', 'You do not want to use config file') 37 | .option('--disable-webpack-config', 'You do not want to use webpack config file') 38 | .option('-o, --open', 'Open browser') 39 | .option('--proxy [url]', 'Proxy API request') 40 | .option('--lib [libraryName]', 'Distribute component in UMD format') 41 | .option('--disable-compress', 'Disable compress when building in production mode') 42 | .parse(process.argv) 43 | 44 | var args = program.args 45 | var production = program.production 46 | 47 | process.env.NODE_ENV = production ? 'production' : 'development' 48 | 49 | var localConfig 50 | 51 | // config option in only available in CLI option 52 | if (!program.disableConfig) { 53 | var localConfigPath = typeof program.config === 'string' 54 | ? cwd(program.config) 55 | : path.join(home, '.vue', 'config.js') 56 | var hasLocalConfig = fs.existsSync(localConfigPath) 57 | if (hasLocalConfig) { 58 | console.log(`> Using config file at ${chalk.yellow(tildify(localConfigPath))}`) 59 | localConfig = require(localConfigPath) 60 | } 61 | } 62 | 63 | var options = merge({ 64 | port: 4000, 65 | dist: 'dist', 66 | host: 'localhost' 67 | }, localConfig, { 68 | entry: args[0], 69 | config: program.config, 70 | port: program.port, 71 | host: program.host, 72 | open: program.open, 73 | dist: program.dist, 74 | webpack: program.webpack, 75 | disableWebpackConfig: program.disableWebpackConfig, 76 | mount: program.mount, 77 | proxy: program.proxy, 78 | production: program.production, 79 | lib: program.lib, 80 | watch: program.watch, 81 | disableCompress: program.disableCompress 82 | }) 83 | 84 | function help () { 85 | if (!options.config && !options.entry) { 86 | return program.help() 87 | } 88 | } 89 | help() 90 | 91 | var cssOptions = { 92 | extract: production, 93 | sourceMap: true 94 | } 95 | 96 | var postcssOptions = { 97 | plugins: [ 98 | require('autoprefixer')(Object.assign({ 99 | browsers: ['ie > 8', 'last 5 versions'] 100 | }, options.autoprefixer)) 101 | ] 102 | } 103 | 104 | if (options.postcss) { 105 | if (Object.prototype.toString.call(options.postcss) === '[object Object]') { 106 | var plugins = options.postcss.plugins 107 | if (plugins) { 108 | postcssOptions.plugins = postcssOptions.plugins.concat(plugins) 109 | delete options.postcss.plugins 110 | } 111 | Object.assign(postcssOptions, options.postcss) 112 | } else { 113 | postcssOptions = options.postcss 114 | } 115 | } 116 | 117 | var babelOptions = Object.assign({ 118 | babelrc: true, 119 | cacheDirectory: true, 120 | sourceMaps: production ? 'both' : false, 121 | presets: [] 122 | }, options.babel) 123 | 124 | var useBabelRc = babelOptions.babelrc && fs.existsSync('.babelrc') 125 | if (useBabelRc) { 126 | console.log('> Using .babelrc in current working directory') 127 | } else { 128 | babelOptions.babelrc = false 129 | babelOptions.presets.push(require.resolve('babel-preset-vue-app')) 130 | } 131 | 132 | var filenames = getFilenames(options) 133 | 134 | var webpackConfig = { 135 | entry: { 136 | client: [] 137 | }, 138 | output: { 139 | path: cwd(options.dist), 140 | filename: filenames.js, 141 | publicPath: '/' 142 | }, 143 | performance: { 144 | hints: false 145 | }, 146 | resolve: { 147 | extensions: ['.js', '.vue', '.css'], 148 | modules: [ 149 | cwd(), 150 | cwd('node_modules'), // modules in cwd's node_modules 151 | ownDir('node_modules') // modules in package's node_modules 152 | ], 153 | alias: { 154 | '@': path.resolve('src') 155 | } 156 | }, 157 | resolveLoader: { 158 | modules: [ 159 | cwd('node_modules'), // loaders in cwd's node_modules 160 | ownDir('node_modules') // loaders in package's node_modules 161 | ] 162 | }, 163 | module: { 164 | rules: [ 165 | { 166 | test: /\.js$/, 167 | loader: 'babel-loader', 168 | exclude: [/node_modules/] 169 | }, 170 | { 171 | test: /\.es6$/, 172 | loader: 'babel-loader' 173 | }, 174 | { 175 | test: /\.vue$/, 176 | loader: 'vue-loader', 177 | options: { 178 | postcss: postcssOptions, 179 | loaders: loaders.cssLoaders(cssOptions) 180 | } 181 | }, 182 | { 183 | test: /\.(ico|jpg|png|gif|svg|eot|otf|webp|ttf|woff|woff2)(\?.*)?$/, 184 | loader: 'file-loader', 185 | query: { 186 | name: filenames.static 187 | } 188 | } 189 | ].concat(loaders.styleLoaders(cssOptions)) 190 | }, 191 | plugins: [ 192 | new webpack.DefinePlugin({ 193 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) 194 | }), 195 | new webpack.LoaderOptionsPlugin({ 196 | options: { 197 | context: process.cwd(), 198 | postcss: postcssOptions, 199 | babel: babelOptions 200 | } 201 | }) 202 | ] 203 | } 204 | 205 | // copy ./static/** to dist folder 206 | if (fs.existsSync('static')) { 207 | webpackConfig.plugins.push(new CopyPlugin([{ 208 | from: 'static', 209 | to: './' 210 | }])) 211 | } 212 | 213 | // if entry ends with `.vue` and no `mount` option was specified 214 | // we implicitly set `mount` to true unless `lib` is set 215 | // for `.js` component you can set `mount` to true manually 216 | if (options.mount === undefined && !options.lib && /\.vue$/.test(options.entry)) { 217 | options.mount = true 218 | } 219 | // create a Vue instance to load given component 220 | // set an alias to the path of the component 221 | // otherwise use it directly as webpack entry 222 | if (options.mount) { 223 | webpackConfig.entry.client.push(ownDir('lib/default-entry.es6')) 224 | webpackConfig.resolve.alias['your-tasteful-component'] = cwd(options.entry) 225 | } else if (Array.isArray(options.entry)) { 226 | webpackConfig.entry.client = options.client 227 | } else if (typeof options.entry === 'object') { 228 | webpackConfig.entry = options.entry 229 | } else { 230 | webpackConfig.entry.client.push(options.entry) 231 | } 232 | 233 | // the `lib` mode 234 | // distribute the entry file as a component (umd format) 235 | if (options.lib) { 236 | webpackConfig.output.filename = replaceExtension(options.entry, '.js') 237 | webpackConfig.output.library = typeof options.lib === 'string' 238 | ? options.lib 239 | : getLibraryName(replaceExtension(options.entry, '')) 240 | webpackConfig.output.libraryTarget = 'umd' 241 | } else if (options.html !== false) { 242 | // only output index.html in non-lib mode 243 | var html = Array.isArray(options.html) ? options.html : [options.html || {}] 244 | 245 | html.forEach(item => { 246 | webpackConfig.plugins.unshift( 247 | new HtmlWebpackPlugin(Object.assign({ 248 | title: 'Vue App', 249 | template: ownDir('lib/template.html') 250 | }, item)) 251 | ) 252 | }) 253 | } 254 | 255 | // installed by `yarn global add` 256 | if (isYarn(__dirname)) { 257 | // modules in yarn global node_modules 258 | // because of yarn's flat node_modules structure 259 | webpackConfig.resolve.modules.push(ownDir('..')) 260 | // loaders in yarn global node_modules 261 | webpackConfig.resolveLoader.modules.push(ownDir('..')) 262 | } 263 | 264 | if (production) { 265 | webpackConfig.plugins.push( 266 | new ProgressPlugin(), 267 | new webpack.LoaderOptionsPlugin({ 268 | minimize: !options.disableCompress 269 | }), 270 | new ExtractTextPlugin(filenames.css) 271 | ) 272 | 273 | if (options.disableCompress) { 274 | webpackConfig.devtool = false 275 | } else { 276 | webpackConfig.devtool = 'source-map' 277 | webpackConfig.plugins.push(new webpack.optimize.UglifyJsPlugin({ 278 | sourceMap: true, 279 | compressor: { 280 | warnings: false, 281 | conditionals: true, 282 | unused: true, 283 | comparisons: true, 284 | sequences: true, 285 | dead_code: true, 286 | evaluate: true, 287 | if_return: true, 288 | join_vars: true, 289 | negate_iife: false 290 | }, 291 | output: { 292 | comments: false 293 | } 294 | })) 295 | } 296 | } else { 297 | webpackConfig.devtool = 'eval-source-map' 298 | webpackConfig.plugins.push( 299 | new FriendlyErrorsPlugin(), 300 | new PostCompilePlugin(() => { 301 | console.log(`> Open http://${options.host}:${options.port}`) 302 | }) 303 | ) 304 | } 305 | 306 | // webpack 307 | // object: merge with webpack config 308 | // function: mutate webpack config 309 | // string: load webpack config file then merge with webpack config 310 | if (!options.disableWebpackConfig) { 311 | if (typeof options.webpack === 'object') { 312 | webpackConfig = webpackMerge.smart(webpackConfig, options.webpack) 313 | } else if (typeof options.webpack === 'function') { 314 | webpackConfig = options.webpack(webpackConfig, options, webpack) || webpackConfig 315 | } else { 316 | var localWebpackConfigPath = typeof options.webpack === 'string' 317 | ? cwd(options.webpack) 318 | : path.join(home, '.vue', 'webpack.config.js') 319 | var hasLocalWebpackConfig = fs.existsSync(localWebpackConfigPath) 320 | if (hasLocalWebpackConfig) { 321 | console.log(`> Using webpack config file at ${chalk.yellow(tildify(localWebpackConfigPath))}`) 322 | webpackConfig = webpackMerge.smart(webpackConfig, require(localWebpackConfigPath)) 323 | } 324 | } 325 | } 326 | 327 | if (!options.watch && !options.production) { 328 | webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin()) 329 | var hmrEntries = options.hmrEntries || ['client'] 330 | var hmrClient = require.resolve('webpack-hot-middleware/client') + `?reload=true&path=http://${options.host}:${options.port}/__webpack_hmr` 331 | hmrEntries.forEach(name => { 332 | if (Array.isArray(webpackConfig.entry[name])) { 333 | webpackConfig.entry[name].unshift(hmrClient) 334 | } else { 335 | webpackConfig.entry[name] = [hmrClient, webpackConfig.entry[name]] 336 | } 337 | }) 338 | } 339 | 340 | // only check entry when there's no custom config 341 | if (!options.config && !fs.existsSync(options.entry)) { 342 | logger.fatal(`${chalk.yellow(options.entry)} does not exist, did you forget to create one?`) 343 | } 344 | 345 | run(webpackConfig, options) 346 | 347 | function merge (obj) { 348 | var i = 1 349 | var target 350 | var key 351 | 352 | for (; i < arguments.length; i++) { 353 | target = arguments[i] 354 | for (key in target) { 355 | if (Object.prototype.hasOwnProperty.call(target, key) && target[key] !== undefined) { 356 | obj[key] = target[key] 357 | } 358 | } 359 | } 360 | 361 | return obj 362 | } 363 | 364 | function replaceExtension (entry, ext) { 365 | return path.basename(entry).replace(/\.(vue|js)$/, ext) 366 | } 367 | 368 | function getLibraryName (fileName) { 369 | return fileName.replace(/[-_.]([\w])/, (_, p1) => p1.toUpperCase()) 370 | } 371 | 372 | function getFilenames (options) { 373 | return Object.assign({ 374 | js: options.production ? '[name].[chunkhash:8].js' : '[name].js', 375 | css: options.lib ? `${replaceExtension(options.entry, '.css')}` : '[name].[contenthash:8].css', 376 | static: options.lib ? 'static/[name].[ext]' : 'static/[name].[hash:8].[ext]' 377 | }, options.filename) 378 | } 379 | 380 | function cwd (file) { 381 | return path.resolve(file || '') 382 | } 383 | 384 | function ownDir (file) { 385 | return path.join(__dirname, '..', file || '') 386 | } 387 | --------------------------------------------------------------------------------