zeus
4 years ago
2 changed files with 380 additions and 0 deletions
@ -0,0 +1,379 @@ |
|||
const { |
|||
info, |
|||
error, |
|||
hasProjectYarn, |
|||
hasProjectPnpm, |
|||
openBrowser, |
|||
IpcMessenger |
|||
} = require('@vue/cli-shared-utils') |
|||
|
|||
const defaults = { |
|||
host: '0.0.0.0', |
|||
//port: 8080,
|
|||
port: 3088, |
|||
https: false |
|||
} |
|||
|
|||
|
|||
module.exports = (api, 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 url = require('url') |
|||
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') { |
|||
webpackConfig |
|||
.devtool('eval-cheap-module-source-map') |
|||
|
|||
webpackConfig |
|||
.plugin('hmr') |
|||
.use(require('webpack/lib/HotModuleReplacementPlugin')) |
|||
|
|||
// 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.progress !== false) { |
|||
webpackConfig |
|||
.plugin('progress') |
|||
.use(require('webpack/lib/ProgressPlugin')) |
|||
} |
|||
} |
|||
}) |
|||
|
|||
// 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 = 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 useHttps = args.https || projectDevServerOptions.https || 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 urls = prepareURLs( |
|||
protocol, |
|||
host, |
|||
port, |
|||
isAbsoluteUrl(options.publicPath) ? '/' : options.publicPath |
|||
) |
|||
const localUrlForBrowser = publicUrl || urls.localUrlForBrowser |
|||
|
|||
const proxySettings = prepareProxy( |
|||
projectDevServerOptions.proxy, |
|||
api.resolve('public') |
|||
) |
|||
|
|||
// inject dev & hot-reload middleware entries
|
|||
if (!isProduction) { |
|||
const sockPath = projectDevServerOptions.sockPath || '/sockjs-node' |
|||
const sockjsUrl = publicUrl |
|||
// explicitly configured via devServer.public
|
|||
? `?${publicUrl}&sockPath=${sockPath}` |
|||
: isInContainer |
|||
// can't infer public network url if inside a container...
|
|||
// use client-side inference (note this would break with non-root publicPath)
|
|||
? `` |
|||
// otherwise infer the url
|
|||
: `?` + url.format({ |
|||
protocol, |
|||
port, |
|||
hostname: urls.lanUrlForConfig || 'localhost' |
|||
}) + `&sockPath=${sockPath}` |
|||
const devClients = [ |
|||
// dev server client
|
|||
require.resolve(`webpack-dev-server/client`) + sockjsUrl, |
|||
// hmr client
|
|||
require.resolve(projectDevServerOptions.hotOnly |
|||
? 'webpack/hot/only-dev-server' |
|||
: 'webpack/hot/dev-server') |
|||
// TODO custom overlay client
|
|||
// `@vue/cli-overlay/dist/client`
|
|||
] |
|||
if (process.env.APPVEYOR) { |
|||
devClients.push(`webpack/hot/poll?500`) |
|||
} |
|||
// inject dev/hot client
|
|||
addDevClientToEntry(webpackConfig, devClients) |
|||
} |
|||
|
|||
// 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(compiler, Object.assign({ |
|||
logLevel: 'silent', |
|||
clientLogLevel: 'silent', |
|||
historyApiFallback: { |
|||
disableDotRule: true, |
|||
rewrites: genHistoryApiFallbackRewrites(options.publicPath, options.pages) |
|||
}, |
|||
contentBase: api.resolve('public'), |
|||
watchContentBase: !isProduction, |
|||
hot: !isProduction, |
|||
injectClient: false, |
|||
compress: isProduction, |
|||
publicPath: options.publicPath, |
|||
overlay: isProduction // TODO disable this
|
|||
? false |
|||
: { warnings: false, errors: true } |
|||
}, projectDevServerOptions, { |
|||
https: useHttps, |
|||
proxy: proxySettings, |
|||
// eslint-disable-next-line no-shadow
|
|||
before (app, server) { |
|||
// launch editor support.
|
|||
// this works with vue-devtools & @vue/cli-overlay
|
|||
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
|
|||
api.service.devServerConfigFns.forEach(fn => fn(app, server)) |
|||
// apply in project middlewares
|
|||
projectDevServerOptions.before && projectDevServerOptions.before(app, server) |
|||
}, |
|||
// avoid opening browser
|
|||
open: false |
|||
})) |
|||
|
|||
;['SIGINT', 'SIGTERM'].forEach(signal => { |
|||
process.on(signal, () => { |
|||
server.close(() => { |
|||
process.exit(0) |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
if (args.stdin) { |
|||
process.stdin.on('end', () => { |
|||
server.close(() => { |
|||
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.close(() => { |
|||
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:<your container's external mapped port>${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() |
|||
|
|||
if (args.open || projectDevServerOptions.open) { |
|||
const pageUri = (projectDevServerOptions.openPage && typeof projectDevServerOptions.openPage === 'string') |
|||
? projectDevServerOptions.openPage |
|||
: '' |
|||
openBrowser(localUrlForBrowser + pageUri) |
|||
} |
|||
|
|||
// 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.listen(port, host, err => { |
|||
if (err) { |
|||
reject(err) |
|||
} |
|||
}) |
|||
}) |
|||
}) |
|||
} |
|||
|
|||
function addDevClientToEntry (config, devClient) { |
|||
const { entry } = config |
|||
if (typeof entry === 'object' && !Array.isArray(entry)) { |
|||
Object.keys(entry).forEach((key) => { |
|||
entry[key] = devClient.concat(entry[key]) |
|||
}) |
|||
} else if (typeof entry === 'function') { |
|||
config.entry = entry(devClient) |
|||
} else { |
|||
config.entry = devClient.concat(entry) |
|||
} |
|||
} |
|||
|
|||
// 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' |
|||
} |
Loading…
Reference in new issue