const { info, error, hasProjectYarn, hasProjectPnpm, IpcMessenger } = require('@vue/cli-shared-utils') const getBaseUrl = require('../util/getBaseUrl') const defaults = { host: '0.0.0.0', port: 8080, https: false } /** @type {import('@vue/cli-service').ServicePlugin} */ module.exports = (api, options) => { const baseUrl = getBaseUrl(options) api.registerCommand('serve', { description: 'start development server', usage: 'vue-cli-service serve [options] [entry]', options: { '--open': `open browser on server start`, '--copy': `copy url to clipboard on server start`, '--stdin': `close when stdin ends`, '--mode': `specify env mode (default: development)`, '--host': `specify host (default: ${defaults.host})`, '--port': `specify port (default: ${defaults.port})`, '--https': `use https (default: ${defaults.https})`, '--public': `specify the public network URL for the HMR client`, '--skip-plugins': `comma-separated list of plugin names to skip for this run` } }, async function serve (args) { info('Starting development server...') // although this is primarily a dev server, it is possible that we // are running it in a mode with a production env, e.g. in E2E tests. const isInContainer = checkInContainer() const isProduction = process.env.NODE_ENV === 'production' const { chalk } = require('@vue/cli-shared-utils') const webpack = require('webpack') const WebpackDevServer = require('webpack-dev-server') const portfinder = require('portfinder') const prepareURLs = require('../util/prepareURLs') const prepareProxy = require('../util/prepareProxy') const launchEditorMiddleware = require('launch-editor-middleware') const validateWebpackConfig = require('../util/validateWebpackConfig') const isAbsoluteUrl = require('../util/isAbsoluteUrl') // configs that only matters for dev server api.chainWebpack(webpackConfig => { if (process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test') { if (!webpackConfig.get('devtool')) { webpackConfig .devtool('eval-cheap-module-source-map') } // https://github.com/webpack/webpack/issues/6642 // https://github.com/vuejs/vue-cli/issues/3539 webpackConfig .output .globalObject(`(typeof self !== 'undefined' ? self : this)`) if ( !process.env.VUE_CLI_TEST && (!options.devServer.client || options.devServer.client.progress !== false) ) { // the default progress plugin won't show progress due to infrastructreLogging.level webpackConfig .plugin('progress') .use(require('progress-webpack-plugin')) } } }) // resolve webpack config const webpackConfig = api.resolveWebpackConfig() // check for common config errors validateWebpackConfig(webpackConfig, api, options) // load user devServer options with higher priority than devServer // in webpack config const projectDevServerOptions = Object.assign( webpackConfig.devServer || {}, options.devServer ) // expose advanced stats if (args.dashboard) { const DashboardPlugin = require('../webpack/DashboardPlugin') webpackConfig.plugins.push(new DashboardPlugin({ type: 'serve' })) } // entry arg const entry = args._[0] if (entry) { webpackConfig.entry = { app: api.resolve(entry) } } // resolve server options const modesUseHttps = ['https', 'http2'] const serversUseHttps = ['https', 'spdy'] const optionsUseHttps = modesUseHttps.some(modeName => !!projectDevServerOptions[modeName]) || (typeof projectDevServerOptions.server === 'string' && serversUseHttps.includes(projectDevServerOptions.server)) || (typeof projectDevServerOptions.server === 'object' && projectDevServerOptions.server !== null && serversUseHttps.includes(projectDevServerOptions.server.type)) const useHttps = args.https || optionsUseHttps || defaults.https const protocol = useHttps ? 'https' : 'http' const host = args.host || process.env.HOST || projectDevServerOptions.host || defaults.host portfinder.basePort = args.port || process.env.PORT || projectDevServerOptions.port || defaults.port const port = await portfinder.getPortPromise() const rawPublicUrl = args.public || projectDevServerOptions.public const publicUrl = rawPublicUrl ? /^[a-zA-Z]+:\/\//.test(rawPublicUrl) ? rawPublicUrl : `${protocol}://${rawPublicUrl}` : null const publicHost = publicUrl ? /^[a-zA-Z]+:\/\/([^/?#]+)/.exec(publicUrl)[1] : undefined const urls = prepareURLs( protocol, host, port, isAbsoluteUrl(baseUrl) ? '/' : baseUrl ) const localUrlForBrowser = publicUrl || urls.localUrlForBrowser const proxySettings = prepareProxy( projectDevServerOptions.proxy, api.resolve('public') ) // inject dev & hot-reload middleware entries let webSocketURL if (!isProduction) { if (publicHost) { // explicitly configured via devServer.public webSocketURL = { protocol: protocol === 'https' ? 'wss' : 'ws', hostname: publicHost, port } } else if (isInContainer) { // can't infer public network url if inside a container // infer it from the browser instead webSocketURL = 'auto://0.0.0.0:0/ws' } else { // otherwise infer the url from the config webSocketURL = { protocol: protocol === 'https' ? 'wss' : 'ws', hostname: urls.lanUrlForConfig || 'localhost', port } } if (process.env.APPVEYOR) { webpackConfig.plugins.push( new webpack.EntryPlugin(__dirname, 'webpack/hot/poll?500', { name: undefined }) ) } } const { projectTargets } = require('../util/targets') const supportsIE = !!projectTargets if (supportsIE) { webpackConfig.plugins.push( // must use undefined as name, // to avoid dev server establishing an extra ws connection for the new entry new webpack.EntryPlugin(__dirname, 'whatwg-fetch', { name: undefined }) ) } // fixme: temporary fix to suppress dev server logging // should be more robust to show necessary info but not duplicate errors webpackConfig.infrastructureLogging = { ...webpackConfig.infrastructureLogging, level: 'none' } webpackConfig.stats = 'errors-only' // create compiler const compiler = webpack(webpackConfig) // handle compiler error compiler.hooks.failed.tap('vue-cli-service serve', msg => { error(msg) process.exit(1) }) // create server const server = new WebpackDevServer(Object.assign({ historyApiFallback: { disableDotRule: true, htmlAcceptHeaders: [ 'text/html', 'application/xhtml+xml' ], rewrites: genHistoryApiFallbackRewrites(baseUrl, options.pages) }, hot: !isProduction }, projectDevServerOptions, { host, port, server: { type: protocol, ...(typeof projectDevServerOptions.server === 'object' ? projectDevServerOptions.server : {}) }, proxy: proxySettings, static: { directory: api.resolve('public'), publicPath: options.publicPath, watch: !isProduction, ...projectDevServerOptions.static }, client: { webSocketURL, logging: 'none', overlay: isProduction // TODO disable this ? false : { warnings: false, errors: true }, progress: !process.env.VUE_CLI_TEST, ...projectDevServerOptions.client }, open: args.open || projectDevServerOptions.open, setupExitSignals: true, setupMiddlewares (middlewares, devServer) { // launch editor support. // this works with vue-devtools & @vue/cli-overlay devServer.app.use('/__open-in-editor', launchEditorMiddleware(() => console.log( `To specify an editor, specify the EDITOR env variable or ` + `add "editor" field to your Vue project config.\n` ))) // allow other plugins to register middlewares, e.g. PWA // todo: migrate to the new API interface api.service.devServerConfigFns.forEach(fn => fn(devServer.app, devServer)) if (projectDevServerOptions.setupMiddlewares) { return projectDevServerOptions.setupMiddlewares(middlewares, devServer) } return middlewares } }), compiler) if (args.stdin) { process.stdin.on('end', () => { server.stopCallback(() => { process.exit(0) }) }) process.stdin.resume() } // on appveyor, killing the process with SIGTERM causes execa to // throw error if (process.env.VUE_CLI_TEST) { process.stdin.on('data', data => { if (data.toString() === 'close') { console.log('got close signal!') server.stopCallback(() => { process.exit(0) }) } }) } return new Promise((resolve, reject) => { // log instructions & open browser on first compilation complete let isFirstCompile = true compiler.hooks.done.tap('vue-cli-service serve', stats => { if (stats.hasErrors()) { return } let copied = '' if (isFirstCompile && args.copy) { try { require('clipboardy').writeSync(localUrlForBrowser) copied = chalk.dim('(copied to clipboard)') } catch (_) { /* catch exception if copy to clipboard isn't supported (e.g. WSL), see issue #3476 */ } } const networkUrl = publicUrl ? publicUrl.replace(/([^/])$/, '$1/') : urls.lanUrlForTerminal console.log() console.log(` App running at:`) console.log(` - Local: ${chalk.cyan(urls.localUrlForTerminal)} ${copied}`) if (!isInContainer) { console.log(` - Network: ${chalk.cyan(networkUrl)}`) } else { console.log() console.log(chalk.yellow(` It seems you are running Vue CLI inside a container.`)) if (!publicUrl && options.publicPath && options.publicPath !== '/') { console.log() console.log(chalk.yellow(` Since you are using a non-root publicPath, the hot-reload socket`)) console.log(chalk.yellow(` will not be able to infer the correct URL to connect. You should`)) console.log(chalk.yellow(` explicitly specify the URL via ${chalk.blue(`devServer.public`)}.`)) console.log() } console.log(chalk.yellow(` Access the dev server via ${chalk.cyan( `${protocol}://localhost:${options.publicPath}` )}`)) } console.log() if (isFirstCompile) { isFirstCompile = false if (!isProduction) { const buildCommand = hasProjectYarn(api.getCwd()) ? `yarn build` : hasProjectPnpm(api.getCwd()) ? `pnpm run build` : `npm run build` console.log(` Note that the development build is not optimized.`) console.log(` To create a production build, run ${chalk.cyan(buildCommand)}.`) } else { console.log(` App is served in production mode.`) console.log(` Note this is for preview or E2E testing only.`) } console.log() // Send final app URL if (args.dashboard) { const ipc = new IpcMessenger() ipc.send({ vueServe: { url: localUrlForBrowser } }) } // resolve returned Promise // so other commands can do api.service.run('serve').then(...) resolve({ server, url: localUrlForBrowser }) } else if (process.env.VUE_CLI_TEST) { // signal for test to check HMR console.log('App updated') } }) server.start().catch(err => reject(err)) }) }) } // https://stackoverflow.com/a/20012536 function checkInContainer () { if ('CODESANDBOX_SSE' in process.env) { return true } const fs = require('fs') if (fs.existsSync(`/proc/1/cgroup`)) { const content = fs.readFileSync(`/proc/1/cgroup`, 'utf-8') return /:\/(lxc|docker|kubepods(\.slice)?)\//.test(content) } } function genHistoryApiFallbackRewrites (baseUrl, pages = {}) { const path = require('path') const multiPageRewrites = Object .keys(pages) // sort by length in reversed order to avoid overrides // eg. 'page11' should appear in front of 'page1' .sort((a, b) => b.length - a.length) .map(name => ({ from: new RegExp(`^/${name}`), to: path.posix.join(baseUrl, pages[name].filename || `${name}.html`) })) return [ ...multiPageRewrites, { from: /./, to: path.posix.join(baseUrl, 'index.html') } ] } module.exports.defaultModes = { serve: 'development' }