serve.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. const {
  2. info,
  3. error,
  4. hasProjectYarn,
  5. hasProjectPnpm,
  6. openBrowser,
  7. IpcMessenger
  8. } = require('@vue/cli-shared-utils')
  9. const defaults = {
  10. host: '0.0.0.0',
  11. port: 8080,
  12. https: false
  13. }
  14. module.exports = (api, options) => {
  15. api.registerCommand('serve', {
  16. description: 'start development server',
  17. usage: 'vue-cli-service serve [options] [entry]',
  18. options: {
  19. '--open': `open browser on server start`,
  20. '--copy': `copy url to clipboard on server start`,
  21. '--stdin': `close when stdin ends`,
  22. '--mode': `specify env mode (default: development)`,
  23. '--host': `specify host (default: ${defaults.host})`,
  24. '--port': `specify port (default: ${defaults.port})`,
  25. '--https': `use https (default: ${defaults.https})`,
  26. '--public': `specify the public network URL for the HMR client`,
  27. '--skip-plugins': `comma-separated list of plugin names to skip for this run`
  28. }
  29. }, async function serve (args) {
  30. info('Starting development server...')
  31. // although this is primarily a dev server, it is possible that we
  32. // are running it in a mode with a production env, e.g. in E2E tests.
  33. const isInContainer = checkInContainer()
  34. const isProduction = process.env.NODE_ENV === 'production'
  35. const url = require('url')
  36. const { chalk } = require('@vue/cli-shared-utils')
  37. const webpack = require('webpack')
  38. const WebpackDevServer = require('webpack-dev-server')
  39. const portfinder = require('portfinder')
  40. const prepareURLs = require('../util/prepareURLs')
  41. const prepareProxy = require('../util/prepareProxy')
  42. const launchEditorMiddleware = require('launch-editor-middleware')
  43. const validateWebpackConfig = require('../util/validateWebpackConfig')
  44. const isAbsoluteUrl = require('../util/isAbsoluteUrl')
  45. // configs that only matters for dev server
  46. api.chainWebpack(webpackConfig => {
  47. if (process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test') {
  48. webpackConfig
  49. .devtool('eval-cheap-module-source-map')
  50. webpackConfig
  51. .plugin('hmr')
  52. .use(require('webpack/lib/HotModuleReplacementPlugin'))
  53. // https://github.com/webpack/webpack/issues/6642
  54. // https://github.com/vuejs/vue-cli/issues/3539
  55. webpackConfig
  56. .output
  57. .globalObject(`(typeof self !== 'undefined' ? self : this)`)
  58. if (!process.env.VUE_CLI_TEST && options.devServer.progress !== false) {
  59. webpackConfig
  60. .plugin('progress')
  61. .use(require('webpack/lib/ProgressPlugin'))
  62. }
  63. }
  64. })
  65. // resolve webpack config
  66. const webpackConfig = api.resolveWebpackConfig()
  67. // check for common config errors
  68. validateWebpackConfig(webpackConfig, api, options)
  69. // load user devServer options with higher priority than devServer
  70. // in webpack config
  71. const projectDevServerOptions = Object.assign(
  72. webpackConfig.devServer || {},
  73. options.devServer
  74. )
  75. // expose advanced stats
  76. if (args.dashboard) {
  77. const DashboardPlugin = require('../webpack/DashboardPlugin')
  78. ;(webpackConfig.plugins = webpackConfig.plugins || []).push(new DashboardPlugin({
  79. type: 'serve'
  80. }))
  81. }
  82. // entry arg
  83. const entry = args._[0]
  84. if (entry) {
  85. webpackConfig.entry = {
  86. app: api.resolve(entry)
  87. }
  88. }
  89. // resolve server options
  90. const useHttps = args.https || projectDevServerOptions.https || defaults.https
  91. const protocol = useHttps ? 'https' : 'http'
  92. const host = args.host || process.env.HOST || projectDevServerOptions.host || defaults.host
  93. portfinder.basePort = args.port || process.env.PORT || projectDevServerOptions.port || defaults.port
  94. const port = await portfinder.getPortPromise()
  95. const rawPublicUrl = args.public || projectDevServerOptions.public
  96. const publicUrl = rawPublicUrl
  97. ? /^[a-zA-Z]+:\/\//.test(rawPublicUrl)
  98. ? rawPublicUrl
  99. : `${protocol}://${rawPublicUrl}`
  100. : null
  101. const urls = prepareURLs(
  102. protocol,
  103. host,
  104. port,
  105. isAbsoluteUrl(options.publicPath) ? '/' : options.publicPath
  106. )
  107. const localUrlForBrowser = publicUrl || urls.localUrlForBrowser
  108. const proxySettings = prepareProxy(
  109. projectDevServerOptions.proxy,
  110. api.resolve('public')
  111. )
  112. // inject dev & hot-reload middleware entries
  113. if (!isProduction) {
  114. const sockPath = projectDevServerOptions.sockPath || '/sockjs-node'
  115. const sockjsUrl = publicUrl
  116. // explicitly configured via devServer.public
  117. ? `?${publicUrl}&sockPath=${sockPath}`
  118. : isInContainer
  119. // can't infer public network url if inside a container...
  120. // use client-side inference (note this would break with non-root publicPath)
  121. ? ``
  122. // otherwise infer the url
  123. : `?` + url.format({
  124. protocol,
  125. port,
  126. hostname: urls.lanUrlForConfig || 'localhost'
  127. }) + `&sockPath=${sockPath}`
  128. const devClients = [
  129. // dev server client
  130. require.resolve(`webpack-dev-server/client`) + sockjsUrl,
  131. // hmr client
  132. require.resolve(projectDevServerOptions.hotOnly
  133. ? 'webpack/hot/only-dev-server'
  134. : 'webpack/hot/dev-server')
  135. // TODO custom overlay client
  136. // `@vue/cli-overlay/dist/client`
  137. ]
  138. if (process.env.APPVEYOR) {
  139. devClients.push(`webpack/hot/poll?500`)
  140. }
  141. // inject dev/hot client
  142. addDevClientToEntry(webpackConfig, devClients)
  143. }
  144. // create compiler
  145. const compiler = webpack(webpackConfig)
  146. // handle compiler error
  147. compiler.hooks.failed.tap('vue-cli-service serve', msg => {
  148. error(msg)
  149. process.exit(1)
  150. })
  151. // create server
  152. const server = new WebpackDevServer(compiler, Object.assign({
  153. logLevel: 'silent',
  154. clientLogLevel: 'silent',
  155. historyApiFallback: {
  156. disableDotRule: true,
  157. rewrites: genHistoryApiFallbackRewrites(options.publicPath, options.pages)
  158. },
  159. contentBase: api.resolve('public'),
  160. watchContentBase: !isProduction,
  161. hot: !isProduction,
  162. injectClient: false,
  163. compress: isProduction,
  164. publicPath: options.publicPath,
  165. overlay: isProduction // TODO disable this
  166. ? false
  167. : { warnings: false, errors: true }
  168. }, projectDevServerOptions, {
  169. https: useHttps,
  170. proxy: proxySettings,
  171. // eslint-disable-next-line no-shadow
  172. before (app, server) {
  173. // launch editor support.
  174. // this works with vue-devtools & @vue/cli-overlay
  175. app.use('/__open-in-editor', launchEditorMiddleware(() => console.log(
  176. `To specify an editor, specify the EDITOR env variable or ` +
  177. `add "editor" field to your Vue project config.\n`
  178. )))
  179. // allow other plugins to register middlewares, e.g. PWA
  180. api.service.devServerConfigFns.forEach(fn => fn(app, server))
  181. // apply in project middlewares
  182. projectDevServerOptions.before && projectDevServerOptions.before(app, server)
  183. },
  184. // avoid opening browser
  185. open: false
  186. }))
  187. ;['SIGINT', 'SIGTERM'].forEach(signal => {
  188. process.on(signal, () => {
  189. server.close(() => {
  190. process.exit(0)
  191. })
  192. })
  193. })
  194. if (args.stdin) {
  195. process.stdin.on('end', () => {
  196. server.close(() => {
  197. process.exit(0)
  198. })
  199. })
  200. process.stdin.resume()
  201. }
  202. // on appveyor, killing the process with SIGTERM causes execa to
  203. // throw error
  204. if (process.env.VUE_CLI_TEST) {
  205. process.stdin.on('data', data => {
  206. if (data.toString() === 'close') {
  207. console.log('got close signal!')
  208. server.close(() => {
  209. process.exit(0)
  210. })
  211. }
  212. })
  213. }
  214. return new Promise((resolve, reject) => {
  215. // log instructions & open browser on first compilation complete
  216. let isFirstCompile = true
  217. compiler.hooks.done.tap('vue-cli-service serve', stats => {
  218. if (stats.hasErrors()) {
  219. return
  220. }
  221. let copied = ''
  222. if (isFirstCompile && args.copy) {
  223. try {
  224. require('clipboardy').writeSync(localUrlForBrowser)
  225. copied = chalk.dim('(copied to clipboard)')
  226. } catch (_) {
  227. /* catch exception if copy to clipboard isn't supported (e.g. WSL), see issue #3476 */
  228. }
  229. }
  230. const networkUrl = publicUrl
  231. ? publicUrl.replace(/([^/])$/, '$1/')
  232. : urls.lanUrlForTerminal
  233. console.log()
  234. console.log(` App running at:`)
  235. console.log(` - Local: ${chalk.cyan(urls.localUrlForTerminal)} ${copied}`)
  236. if (!isInContainer) {
  237. console.log(` - Network: ${chalk.cyan(networkUrl)}`)
  238. } else {
  239. console.log()
  240. console.log(chalk.yellow(` It seems you are running Vue CLI inside a container.`))
  241. if (!publicUrl && options.publicPath && options.publicPath !== '/') {
  242. console.log()
  243. console.log(chalk.yellow(` Since you are using a non-root publicPath, the hot-reload socket`))
  244. console.log(chalk.yellow(` will not be able to infer the correct URL to connect. You should`))
  245. console.log(chalk.yellow(` explicitly specify the URL via ${chalk.blue(`devServer.public`)}.`))
  246. console.log()
  247. }
  248. console.log(chalk.yellow(` Access the dev server via ${chalk.cyan(
  249. `${protocol}://localhost:<your container's external mapped port>${options.publicPath}`
  250. )}`))
  251. }
  252. console.log()
  253. if (isFirstCompile) {
  254. isFirstCompile = false
  255. if (!isProduction) {
  256. const buildCommand = hasProjectYarn(api.getCwd()) ? `yarn build` : hasProjectPnpm(api.getCwd()) ? `pnpm run build` : `npm run build`
  257. console.log(` Note that the development build is not optimized.`)
  258. console.log(` To create a production build, run ${chalk.cyan(buildCommand)}.`)
  259. } else {
  260. console.log(` App is served in production mode.`)
  261. console.log(` Note this is for preview or E2E testing only.`)
  262. }
  263. console.log()
  264. if (args.open || projectDevServerOptions.open) {
  265. const pageUri = (projectDevServerOptions.openPage && typeof projectDevServerOptions.openPage === 'string')
  266. ? projectDevServerOptions.openPage
  267. : ''
  268. openBrowser(localUrlForBrowser + pageUri)
  269. }
  270. // Send final app URL
  271. if (args.dashboard) {
  272. const ipc = new IpcMessenger()
  273. ipc.send({
  274. vueServe: {
  275. url: localUrlForBrowser
  276. }
  277. })
  278. }
  279. // resolve returned Promise
  280. // so other commands can do api.service.run('serve').then(...)
  281. resolve({
  282. server,
  283. url: localUrlForBrowser
  284. })
  285. } else if (process.env.VUE_CLI_TEST) {
  286. // signal for test to check HMR
  287. console.log('App updated')
  288. }
  289. })
  290. server.listen(port, host, err => {
  291. if (err) {
  292. reject(err)
  293. }
  294. })
  295. })
  296. })
  297. }
  298. function addDevClientToEntry (config, devClient) {
  299. const { entry } = config
  300. if (typeof entry === 'object' && !Array.isArray(entry)) {
  301. Object.keys(entry).forEach((key) => {
  302. entry[key] = devClient.concat(entry[key])
  303. })
  304. } else if (typeof entry === 'function') {
  305. config.entry = entry(devClient)
  306. } else {
  307. config.entry = devClient.concat(entry)
  308. }
  309. }
  310. // https://stackoverflow.com/a/20012536
  311. function checkInContainer () {
  312. if ('CODESANDBOX_SSE' in process.env) {
  313. return true
  314. }
  315. const fs = require('fs')
  316. if (fs.existsSync(`/proc/1/cgroup`)) {
  317. const content = fs.readFileSync(`/proc/1/cgroup`, 'utf-8')
  318. return /:\/(lxc|docker|kubepods(\.slice)?)\//.test(content)
  319. }
  320. }
  321. function genHistoryApiFallbackRewrites (baseUrl, pages = {}) {
  322. const path = require('path')
  323. const multiPageRewrites = Object
  324. .keys(pages)
  325. // sort by length in reversed order to avoid overrides
  326. // eg. 'page11' should appear in front of 'page1'
  327. .sort((a, b) => b.length - a.length)
  328. .map(name => ({
  329. from: new RegExp(`^/${name}`),
  330. to: path.posix.join(baseUrl, pages[name].filename || `${name}.html`)
  331. }))
  332. return [
  333. ...multiPageRewrites,
  334. { from: /./, to: path.posix.join(baseUrl, 'index.html') }
  335. ]
  336. }
  337. module.exports.defaultModes = {
  338. serve: 'development'
  339. }