serve.js 13 KB

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