|
| 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 rm = require('rimraf').sync |
| 8 | +var home = require('user-home') |
| 9 | +var webpack = require('webpack') |
| 10 | +var webpackMerge = require('webpack-merge') |
| 11 | +var HtmlWebpackPlugin = require('html-webpack-plugin') |
| 12 | +var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') |
| 13 | +var PostCompilePlugin = require('post-compile-webpack-plugin') |
| 14 | +var ProgressPlugin = require('webpack/lib/ProgressPlugin') |
| 15 | +var ExtractTextPlugin = require('extract-text-webpack-plugin') |
| 16 | +var isYarn = require('installed-by-yarn-globally') |
| 17 | +var camelcase = require('camelcase') |
| 18 | +var tildify = require('tildify') |
| 19 | +var loaders = require('../lib/loaders') |
| 20 | +var logger = require('../lib/logger') |
| 21 | +var createServer = require('../lib/server') |
| 22 | + |
| 23 | +/** |
| 24 | + * Usage. |
| 25 | + */ |
| 26 | + |
| 27 | +program |
| 28 | + .usage('[entry]') |
| 29 | + .option('--dist [directory]', 'Output directory for bundled files') |
| 30 | + .option('--port [port]', 'Server port') |
| 31 | + .option('--host [host]', 'Server host', 'localhost') |
| 32 | + .option('--prod, --production', 'Build for production') |
| 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('-w, --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', 'Proxy API request') |
| 40 | + .option('--lib [libraryName]', 'Distribute component in UMD format') |
| 41 | + .parse(process.argv) |
| 42 | + |
| 43 | +var args = program.args |
| 44 | +var production = program.production |
| 45 | + |
| 46 | +process.env.NODE_ENV = production ? 'production' : 'development' |
| 47 | + |
| 48 | +var localConfig |
| 49 | + |
| 50 | +// config option in only available in CLI option |
| 51 | +if (!program.disableConfig) { |
| 52 | + var localConfigPath = typeof program.config === 'string' |
| 53 | + ? path.join(process.cwd(), program.config) |
| 54 | + : path.join(home, '.vue', 'config.js') |
| 55 | + var hasLocalConfig = fs.existsSync(localConfigPath) |
| 56 | + if (hasLocalConfig) { |
| 57 | + console.log(`> Using config file at ${chalk.yellow(tildify(localConfigPath))}`) |
| 58 | + localConfig = require(localConfigPath) |
| 59 | + } |
| 60 | +} |
| 61 | + |
| 62 | +var options = merge({ |
| 63 | + port: 4000, |
| 64 | + dist: 'dist', |
| 65 | + host: 'localhost' |
| 66 | +}, localConfig, { |
| 67 | + entry: args[0], |
| 68 | + port: program.port, |
| 69 | + host: program.host, |
| 70 | + open: program.open, |
| 71 | + dist: program.dist, |
| 72 | + webpack: program.webpack, |
| 73 | + disableWebpackConfig: program.disableWebpackConfig, |
| 74 | + mount: program.mount, |
| 75 | + proxy: program.proxy, |
| 76 | + production: program.production, |
| 77 | + lib: program.lib |
| 78 | +}) |
| 79 | + |
| 80 | +function help () { |
| 81 | + if (!options.entry) { |
| 82 | + return program.help() |
| 83 | + } |
| 84 | +} |
| 85 | +help() |
| 86 | + |
| 87 | +var cssOptions = { |
| 88 | + extract: production, |
| 89 | + sourceMap: true |
| 90 | +} |
| 91 | + |
| 92 | +var postcssOptions = { |
| 93 | + plugins: [ |
| 94 | + require('autoprefixer')(Object.assign({ |
| 95 | + browsers: ['ie > 8', 'last 5 versions'] |
| 96 | + }, options.autoprefixer)) |
| 97 | + ] |
| 98 | +} |
| 99 | + |
| 100 | +if (options.postcss) { |
| 101 | + if (Object.prototype.toString.call(options.postcss) === '[object Object]') { |
| 102 | + var plugins = options.postcss.plugins |
| 103 | + if (plugins) { |
| 104 | + postcssOptions.plugins = postcssOptions.plugins.concat(plugins) |
| 105 | + delete options.postcss.plugins |
| 106 | + } |
| 107 | + Object.assign(postcssOptions, options.postcss) |
| 108 | + } else { |
| 109 | + postcssOptions = options.postcss |
| 110 | + } |
| 111 | +} |
| 112 | + |
| 113 | +var babelOptions = { |
| 114 | + babelrc: true, |
| 115 | + cacheDirectory: true, |
| 116 | + sourceMaps: production ? 'both' : false, |
| 117 | + presets: [] |
| 118 | +} |
| 119 | + |
| 120 | +var hasBabelRc = fs.existsSync('.babelrc') |
| 121 | +if (hasBabelRc) { |
| 122 | + console.log('> Using .babelrc in current working directory') |
| 123 | +} else { |
| 124 | + babelOptions.presets.push(require.resolve('babel-preset-vue-app')) |
| 125 | +} |
| 126 | + |
| 127 | +var webpackConfig = { |
| 128 | + entry: { |
| 129 | + client: [] |
| 130 | + }, |
| 131 | + output: { |
| 132 | + path: path.join(process.cwd(), options.dist), |
| 133 | + filename: production ? '[name].[chunkhash:8].js' : '[name].js', |
| 134 | + publicPath: '/' |
| 135 | + }, |
| 136 | + performance: { |
| 137 | + hints: false |
| 138 | + }, |
| 139 | + resolve: { |
| 140 | + extensions: ['.js', '.vue', '.css'], |
| 141 | + modules: [ |
| 142 | + process.cwd(), |
| 143 | + path.join(process.cwd(), 'node_modules'), // modules in cwd's node_modules |
| 144 | + path.join(__dirname, '../node_modules') // modules in package's node_modules |
| 145 | + ], |
| 146 | + alias: {} |
| 147 | + }, |
| 148 | + resolveLoader: { |
| 149 | + modules: [ |
| 150 | + path.join(process.cwd(), 'node_modules'), // loaders in cwd's node_modules |
| 151 | + path.join(__dirname, '../node_modules') // loaders in package's node_modules |
| 152 | + ] |
| 153 | + }, |
| 154 | + module: { |
| 155 | + rules: [ |
| 156 | + { |
| 157 | + test: /\.js$/, |
| 158 | + loader: 'babel-loader', |
| 159 | + exclude: [/node_modules/] |
| 160 | + }, |
| 161 | + { |
| 162 | + test: /\.vue$/, |
| 163 | + loader: 'vue-loader', |
| 164 | + options: { |
| 165 | + postcss: postcssOptions, |
| 166 | + loaders: loaders.cssLoaders(cssOptions) |
| 167 | + } |
| 168 | + }, |
| 169 | + { |
| 170 | + test: /\.(ico|jpg|png|gif|svg|eot|otf|webp|ttf|woff|woff2)(\?.*)?$/, |
| 171 | + loader: 'file-loader', |
| 172 | + query: { |
| 173 | + name: options.lib ? 'static/[name].[ext]' : 'static/[name].[hash:8].[ext]' |
| 174 | + } |
| 175 | + } |
| 176 | + ].concat(loaders.styleLoaders(cssOptions)) |
| 177 | + }, |
| 178 | + plugins: [ |
| 179 | + new webpack.DefinePlugin({ |
| 180 | + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) |
| 181 | + }), |
| 182 | + new webpack.LoaderOptionsPlugin({ |
| 183 | + options: { |
| 184 | + context: process.cwd(), |
| 185 | + postcss: postcssOptions, |
| 186 | + babel: babelOptions |
| 187 | + } |
| 188 | + }) |
| 189 | + ] |
| 190 | +} |
| 191 | + |
| 192 | +// if entry ends with `.vue` and no `mount` option was specified |
| 193 | +// we implicitly set `mount` to true unless `lib` is set |
| 194 | +// for `.js` component you can set `mount` to true manually |
| 195 | +if (options.mount === undefined && !options.lib && /\.vue$/.test(options.entry)) { |
| 196 | + options.mount = true |
| 197 | +} |
| 198 | +// create a Vue instance to load given component |
| 199 | +// set an alias to the path of the component |
| 200 | +// otherwise use it directly as webpack entry |
| 201 | +if (options.mount) { |
| 202 | + webpackConfig.entry.client.push(path.join(__dirname, '../lib/default-entry')) |
| 203 | + webpackConfig.resolve.alias['your-tasteful-component'] = options.entry |
| 204 | +} else { |
| 205 | + webpackConfig.entry.client.push(options.entry) |
| 206 | +} |
| 207 | + |
| 208 | +// the `lib` mode |
| 209 | +// distribute the entry file as a component (umd format) |
| 210 | +if (options.lib) { |
| 211 | + webpackConfig.output.filename = replaceExtension(options.entry, '.js') |
| 212 | + webpackConfig.output.library = typeof options.lib === 'string' |
| 213 | + ? options.lib |
| 214 | + : camelcase(replaceExtension(options.entry, '')) |
| 215 | + webpackConfig.output.libraryTarget = 'umd' |
| 216 | +} else { |
| 217 | + // only output index.html in non-lib mode |
| 218 | + webpackConfig.plugins.unshift( |
| 219 | + new HtmlWebpackPlugin(Object.assign({ |
| 220 | + title: 'Vue App', |
| 221 | + template: path.join(__dirname, '../lib/template.html') |
| 222 | + }, options.html)) |
| 223 | + ) |
| 224 | +} |
| 225 | + |
| 226 | +// installed by `yarn global add` |
| 227 | +if (isYarn(__dirname)) { |
| 228 | + // modules in yarn global node_modules |
| 229 | + // because of yarn's flat node_modules structure |
| 230 | + webpackConfig.resolve.modules.push(path.join(__dirname, '../../')) |
| 231 | + // loaders in yarn global node_modules |
| 232 | + webpackConfig.resolveLoader.modules.push(path.join(__dirname, '../../')) |
| 233 | +} |
| 234 | + |
| 235 | +if (production) { |
| 236 | + webpackConfig.devtool = 'source-map' |
| 237 | + webpackConfig.plugins.push( |
| 238 | + new ProgressPlugin(), |
| 239 | + new webpack.optimize.UglifyJsPlugin({ |
| 240 | + sourceMap: true, |
| 241 | + compressor: { |
| 242 | + warnings: false |
| 243 | + }, |
| 244 | + output: { |
| 245 | + comments: false |
| 246 | + } |
| 247 | + }), |
| 248 | + new webpack.LoaderOptionsPlugin({ |
| 249 | + minimize: true |
| 250 | + }), |
| 251 | + new ExtractTextPlugin(options.lib ? `${replaceExtension(options.entry, '.css')}` : '[name].[contenthash:8].css') |
| 252 | + ) |
| 253 | +} else { |
| 254 | + webpackConfig.devtool = 'eval-source-map' |
| 255 | + webpackConfig.entry.client.unshift( |
| 256 | + require.resolve('webpack-hot-middleware/client') + '?reload=true' |
| 257 | + ) |
| 258 | + webpackConfig.plugins.push( |
| 259 | + new webpack.HotModuleReplacementPlugin(), |
| 260 | + new FriendlyErrorsPlugin(), |
| 261 | + new PostCompilePlugin(() => { |
| 262 | + console.log(`> Open http://${options.host}:${options.port}`) |
| 263 | + }) |
| 264 | + ) |
| 265 | +} |
| 266 | + |
| 267 | +// webpack |
| 268 | +// object: merge with webpack config |
| 269 | +// function: mutate webpack config |
| 270 | +// string: load webpack config file then merge with webpack config |
| 271 | +if (!options.disableWebpackConfig) { |
| 272 | + if (typeof options.webpack === 'object') { |
| 273 | + webpackConfig = webpackMerge.smart(webpackConfig, options.webpack) |
| 274 | + } else if (typeof options.webpack === 'function') { |
| 275 | + webpackConfig = options.webpack(webpackConfig, options, webpack) |
| 276 | + } else { |
| 277 | + var localWebpackConfigPath = typeof options.webpack === 'string' |
| 278 | + ? path.join(process.cwd(), options.webpack) |
| 279 | + : path.join(home, '.vue', 'webpack.config.js') |
| 280 | + var hasLocalWebpackConfig = fs.existsSync(localWebpackConfigPath) |
| 281 | + if (hasLocalWebpackConfig) { |
| 282 | + console.log(`> Using webpack config file at ${chalk.yellow(tildify(localWebpackConfigPath))}`) |
| 283 | + webpackConfig = webpackMerge.smart(webpackConfig, require(localWebpackConfigPath)) |
| 284 | + } |
| 285 | + } |
| 286 | +} |
| 287 | + |
| 288 | +try { |
| 289 | + var compiler = webpack(webpackConfig) |
| 290 | +} catch (err) { |
| 291 | + if (err.name === 'WebpackOptionsValidationError') { |
| 292 | + logger.fatal(err.message) |
| 293 | + } else { |
| 294 | + throw err |
| 295 | + } |
| 296 | +} |
| 297 | + |
| 298 | +checkEntryExists(options.entry) |
| 299 | + |
| 300 | +if (production) { |
| 301 | + console.log('> Creating an optimized production build:\n') |
| 302 | + // remove dist files but keep that folder in production mode |
| 303 | + rm(path.join(options.dist, '*')) |
| 304 | + compiler.run((err, stats) => { |
| 305 | + if (err) { |
| 306 | + process.exitCode = 1 |
| 307 | + return console.error(err.stack) |
| 308 | + } |
| 309 | + if (stats.hasErrors() || stats.hasWarnings()) { |
| 310 | + process.exitCode = 1 |
| 311 | + return console.error(stats.toString('errors-only')) |
| 312 | + } |
| 313 | + console.log(stats.toString({ |
| 314 | + chunks: false, |
| 315 | + children: false, |
| 316 | + modules: false, |
| 317 | + colors: true |
| 318 | + })) |
| 319 | + console.log(`\n${chalk.bgGreen.black(' SUCCESS ')} Compiled successfully.\n`) |
| 320 | + console.log(`The ${chalk.cyan(options.dist)} folder is ready to be deployed.`) |
| 321 | + console.log(`You may also serve it locally with a static server:\n`) |
| 322 | + console.log(` ${chalk.yellow('npm')} i -g serve`) |
| 323 | + console.log(` ${chalk.yellow('serve')} ${options.dist}\n`) |
| 324 | + }) |
| 325 | +} else { |
| 326 | + var server = createServer(compiler, options) |
| 327 | + |
| 328 | + server.listen(options.port, options.host) |
| 329 | + if (options.open) { |
| 330 | + require('opn')(`http://${options.host}:${options.port}`) |
| 331 | + } |
| 332 | +} |
| 333 | + |
| 334 | +function checkEntryExists (entry) { |
| 335 | + if (!fs.existsSync(entry)) { |
| 336 | + logger.fatal(`${chalk.yellow(entry)} does not exist, did you forget to create one?`) |
| 337 | + } |
| 338 | +} |
| 339 | + |
| 340 | +function merge (obj) { |
| 341 | + var i = 1 |
| 342 | + var target |
| 343 | + var key |
| 344 | + |
| 345 | + for (; i < arguments.length; i++) { |
| 346 | + target = arguments[i] |
| 347 | + for (key in target) { |
| 348 | + if (Object.prototype.hasOwnProperty.call(target, key) && target[key] !== undefined) { |
| 349 | + obj[key] = target[key] |
| 350 | + } |
| 351 | + } |
| 352 | + } |
| 353 | + |
| 354 | + return obj |
| 355 | +} |
| 356 | + |
| 357 | +function replaceExtension (str, ext) { |
| 358 | + return str.replace(/\.(vue|js)$/, ext) |
| 359 | +} |
0 commit comments