├── 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 |
2 | {{ no }}
3 |
4 |
--------------------------------------------------------------------------------
/test/e2e/mock-skip-glob/template/src/yes.vue:
--------------------------------------------------------------------------------
1 |
2 | \{{yes}} {{pick}}
3 |
4 |
--------------------------------------------------------------------------------
/test/e2e/mock-template-repo/template/src/skip-one.vue:
--------------------------------------------------------------------------------
1 | one: {{description}}
2 |
--------------------------------------------------------------------------------
/test/e2e/mock-template-repo/template/src/skip-two.vue:
--------------------------------------------------------------------------------
1 | two: {{description}}
2 |
--------------------------------------------------------------------------------
/test/e2e/mock-template-repo/template/src/yes.vue:
--------------------------------------------------------------------------------
1 |
2 | \{{yes}} {{pick}}
3 |
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 |
2 | hello
3 |
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 [](https://circleci.com/gh/vuejs/vue-cli) [](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 |
--------------------------------------------------------------------------------