proxy + wip overlay

This commit is contained in:
Evan You
2017-12-28 00:47:32 -05:00
parent ce8f8bb48d
commit f1e167f45b
11 changed files with 615 additions and 13 deletions
+1
View File
@@ -3,3 +3,4 @@ node_modules
design
*.log
packages/test
dist
@@ -0,0 +1,12 @@
const launchEditor = require('./launchEditor')
module.exports = () => {
return function launchEditorMiddleware (req, res, next) {
if (req.url.startsWith('/open-in-editor')) {
launchEditor(req.query.fileName, req.query.lineNumber)
res.end()
} else {
next()
}
}
}
@@ -0,0 +1,319 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file at
* https://github.com/facebookincubator/create-react-app/blob/master/LICENSE
*/
const fs = require('fs')
const os = require('os')
const path = require('path')
const chalk = require('chalk')
const shellQuote = require('shell-quote')
const child_process = require('child_process') // eslint-disable-line
function isTerminalEditor (editor) {
switch (editor) {
case 'vim':
case 'emacs':
case 'nano':
return true
}
return false
}
// Map from full process name to binary that starts the process
// We can't just re-use full process name, because it will spawn a new instance
// of the app every time
const COMMON_EDITORS_OSX = {
'/Applications/Atom.app/Contents/MacOS/Atom': 'atom',
'/Applications/Atom Beta.app/Contents/MacOS/Atom Beta':
'/Applications/Atom Beta.app/Contents/MacOS/Atom Beta',
'/Applications/Brackets.app/Contents/MacOS/Brackets': 'brackets',
'/Applications/Sublime Text.app/Contents/MacOS/Sublime Text':
'/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl',
'/Applications/Sublime Text 2.app/Contents/MacOS/Sublime Text 2':
'/Applications/Sublime Text 2.app/Contents/SharedSupport/bin/subl',
'/Applications/Visual Studio Code.app/Contents/MacOS/Electron': 'code',
'/Applications/AppCode.app/Contents/MacOS/appcode':
'/Applications/AppCode.app/Contents/MacOS/appcode',
'/Applications/CLion.app/Contents/MacOS/clion':
'/Applications/CLion.app/Contents/MacOS/clion',
'/Applications/IntelliJ IDEA.app/Contents/MacOS/idea':
'/Applications/IntelliJ IDEA.app/Contents/MacOS/idea',
'/Applications/PhpStorm.app/Contents/MacOS/phpstorm':
'/Applications/PhpStorm.app/Contents/MacOS/phpstorm',
'/Applications/PyCharm.app/Contents/MacOS/pycharm':
'/Applications/PyCharm.app/Contents/MacOS/pycharm',
'/Applications/PyCharm CE.app/Contents/MacOS/pycharm':
'/Applications/PyCharm CE.app/Contents/MacOS/pycharm',
'/Applications/RubyMine.app/Contents/MacOS/rubymine':
'/Applications/RubyMine.app/Contents/MacOS/rubymine',
'/Applications/WebStorm.app/Contents/MacOS/webstorm':
'/Applications/WebStorm.app/Contents/MacOS/webstorm'
}
const COMMON_EDITORS_LINUX = {
atom: 'atom',
Brackets: 'brackets',
code: 'code',
emacs: 'emacs',
'idea.sh': 'idea',
'phpstorm.sh': 'phpstorm',
'pycharm.sh': 'pycharm',
'rubymine.sh': 'rubymine',
sublime_text: 'sublime_text',
vim: 'vim',
'webstorm.sh': 'webstorm'
}
const COMMON_EDITORS_WIN = [
'Brackets.exe',
'Code.exe',
'atom.exe',
'sublime_text.exe',
'notepad++.exe',
'clion.exe',
'clion64.exe',
'idea.exe',
'idea64.exe',
'phpstorm.exe',
'phpstorm64.exe',
'pycharm.exe',
'pycharm64.exe',
'rubymine.exe',
'rubymine64.exe',
'webstorm.exe',
'webstorm64.exe'
]
function addWorkspaceToArgumentsIfExists (args, workspace) {
if (workspace) {
args.unshift(workspace)
}
return args
}
function getArgumentsForLineNumber (editor, fileName, lineNumber, workspace) {
const editorBasename = path.basename(editor).replace(/\.(exe|cmd|bat)$/i, '')
switch (editorBasename) {
case 'atom':
case 'Atom':
case 'Atom Beta':
case 'subl':
case 'sublime':
case 'sublime_text':
case 'wstorm':
case 'charm':
return [fileName + ':' + lineNumber]
case 'notepad++':
return ['-n' + lineNumber, fileName]
case 'vim':
case 'mvim':
case 'joe':
case 'emacs':
case 'emacsclient':
return ['+' + lineNumber, fileName]
case 'rmate':
case 'mate':
case 'mine':
return ['--line', lineNumber, fileName]
case 'code':
case 'Code':
return addWorkspaceToArgumentsIfExists(
['-g', fileName + ':' + lineNumber],
workspace
)
case 'appcode':
case 'clion':
case 'clion64':
case 'idea':
case 'idea64':
case 'phpstorm':
case 'phpstorm64':
case 'pycharm':
case 'pycharm64':
case 'rubymine':
case 'rubymine64':
case 'webstorm':
case 'webstorm64':
return addWorkspaceToArgumentsIfExists(
['--line', lineNumber, fileName],
workspace
)
}
// For all others, drop the lineNumber until we have
// a mapping above, since providing the lineNumber incorrectly
// can result in errors or confusing behavior.
return [fileName]
}
function guessEditor () {
// Explicit config always wins
if (process.env.REACT_EDITOR) {
return shellQuote.parse(process.env.REACT_EDITOR)
}
// We can find out which editor is currently running by:
// `ps x` on macOS and Linux
// `Get-Process` on Windows
try {
if (process.platform === 'darwin') {
const output = child_process.execSync('ps x').toString()
const processNames = Object.keys(COMMON_EDITORS_OSX)
for (let i = 0; i < processNames.length; i++) {
const processName = processNames[i]
if (output.indexOf(processName) !== -1) {
return [COMMON_EDITORS_OSX[processName]]
}
}
} else if (process.platform === 'win32') {
const output = child_process
.execSync('powershell -Command "Get-Process | Select-Object Path"', {
stdio: ['pipe', 'pipe', 'ignore']
})
.toString()
const runningProcesses = output.split('\r\n')
for (let i = 0; i < runningProcesses.length; i++) {
// `Get-Process` sometimes returns empty lines
if (!runningProcesses[i]) {
continue
}
const fullProcessPath = runningProcesses[i].trim()
const shortProcessName = path.basename(fullProcessPath)
if (COMMON_EDITORS_WIN.indexOf(shortProcessName) !== -1) {
return [fullProcessPath]
}
}
} else if (process.platform === 'linux') {
// --no-heading No header line
// x List all processes owned by you
// -o comm Need only names column
const output = child_process
.execSync('ps x --no-heading -o comm --sort=comm')
.toString()
const processNames = Object.keys(COMMON_EDITORS_LINUX)
for (let i = 0; i < processNames.length; i++) {
const processName = processNames[i]
if (output.indexOf(processName) !== -1) {
return [COMMON_EDITORS_LINUX[processName]]
}
}
}
} catch (error) {
// Ignore...
}
// Last resort, use old skool env vars
if (process.env.VISUAL) {
return [process.env.VISUAL]
} else if (process.env.EDITOR) {
return [process.env.EDITOR]
}
return [null]
}
function printInstructions (fileName, errorMessage) {
console.log()
console.log(
chalk.red('Could not open ' + path.basename(fileName) + ' in the editor.')
)
if (errorMessage) {
if (errorMessage[errorMessage.length - 1] !== '.') {
errorMessage += '.'
}
console.log(
chalk.red('The editor process exited with an error: ' + errorMessage)
)
}
console.log()
console.log(
'To set up the editor integration, add something like ' +
chalk.cyan('REACT_EDITOR=atom') +
' to the ' +
chalk.green('.env.local') +
' file in your project folder ' +
'and restart the development server. Learn more: ' +
chalk.green('https://goo.gl/MMTaZt')
)
console.log()
}
let _childProcess = null
function launchEditor (fileName, lineNumber) {
if (!fs.existsSync(fileName)) {
return
}
// Sanitize lineNumber to prevent malicious use on win32
// via: https://github.com/nodejs/node/blob/c3bb4b1aa5e907d489619fb43d233c3336bfc03d/lib/child_process.js#L333
if (lineNumber && isNaN(lineNumber)) {
return
}
let [editor, ...args] = guessEditor() // eslint-disable-line
if (!editor) {
printInstructions(fileName, null)
return
}
if (
process.platform === 'linux' &&
fileName.startsWith('/mnt/') &&
/Microsoft/i.test(os.release())
) {
// Assume WSL / "Bash on Ubuntu on Windows" is being used, and
// that the file exists on the Windows file system.
// `os.release()` is "4.4.0-43-Microsoft" in the current release
// build of WSL, see: https://github.com/Microsoft/BashOnWindows/issues/423#issuecomment-221627364
// When a Windows editor is specified, interop functionality can
// handle the path translation, but only if a relative path is used.
fileName = path.relative('', fileName)
}
const workspace = null
if (lineNumber) {
args = args.concat(
getArgumentsForLineNumber(editor, fileName, lineNumber, workspace)
)
} else {
args.push(fileName)
}
if (_childProcess && isTerminalEditor(editor)) {
// There's an existing editor process already and it's attached
// to the terminal, so go kill it. Otherwise two separate editor
// instances attach to the stdin/stdout which gets confusing.
_childProcess.kill('SIGKILL')
}
if (process.platform === 'win32') {
// On Windows, launch the editor in a shell because spawn can only
// launch .exe files.
_childProcess = child_process.spawn(
'cmd.exe',
['/C', editor].concat(args),
{ stdio: 'inherit' }
)
} else {
_childProcess = child_process.spawn(editor, args, { stdio: 'inherit' })
}
_childProcess.on('exit', function (errorCode) {
_childProcess = null
if (errorCode) {
printInstructions(fileName, '(code ' + errorCode + ')')
}
})
_childProcess.on('error', function (error) {
printInstructions(fileName, error.message)
})
}
module.exports = launchEditor
+29
View File
@@ -0,0 +1,29 @@
{
"name": "@vue/cli-overlay",
"version": "1.0.0",
"description": "error overlay & dev server middleware for vue-cli",
"main": "dist/client.js",
"files": [
"dist",
"middleware"
],
"repository": {
"type": "git",
"url": "git+https://github.com/vuejs/vue-cli.git"
},
"keywords": [
"vue",
"cli",
"cli-overlay"
],
"author": "Evan You",
"license": "MIT",
"bugs": {
"url": "https://github.com/vuejs/vue-cli/issues"
},
"homepage": "https://github.com/vuejs/vue-cli/packages/@vue/cli-overlay#readme",
"dependencies": {
"chalk": "^2.3.0",
"shell-quote": "^1.6.1"
}
}
@@ -34,4 +34,8 @@ module.exports = class PluginAPI {
resolveWebpackConfig () {
return this.service.resolveWebpackConfig()
}
configureDevServer (fn) {
this.service.devServerConfigFns.push(fn)
}
}
+2 -4
View File
@@ -11,6 +11,7 @@ module.exports = class Service {
this.webpackConfig = new Config()
this.webpackChainFns = []
this.webpackRawConfigFns = []
this.devServerConfigFns = []
this.commands = {}
const pkg = getPkg.sync()
@@ -67,10 +68,7 @@ module.exports = class Service {
return fn(args)
}
resolveWebpackConfig (env) {
if (env) {
process.env.NODE_ENV = env
}
resolveWebpackConfig () {
// apply chains
this.webpackChainFns.forEach(fn => fn(this.webpackConfig))
// to raw config
@@ -20,6 +20,8 @@ module.exports = (api, options) => {
const portfinder = require('portfinder')
const openBrowser = require('../util/openBrowser')
const prepareURLs = require('../util/prepareURLs')
const prepareProxy = require('../util/prepareProxy')
const overlayMiddleware = require('@vue/cli-overlay/middleware')
const projectDevServerOptions = options.devServer || {}
const useHttps = args.https || projectDevServerOptions.https
@@ -41,10 +43,14 @@ module.exports = (api, options) => {
// inject dev/hot client
addDevClientToEntry(webpackConfig, [
// dev server client
`webpack-dev-server/client/?${urls.localUrlForBrowser}`,
// hmr client
projectDevServerOptions.hotOnly
? 'webpack/hot/dev-server'
: 'webpack/hot/only-dev-server'
: 'webpack/hot/only-dev-server',
// custom overlay client
`@vue/cli-overlay/dist/client`
])
const compiler = webpack(webpackConfig)
@@ -77,6 +83,11 @@ module.exports = (api, options) => {
}
})
const proxySettings = prepareProxy(
projectDevServerOptions.proxy,
api.resolve('public')
)
const server = new WebpackDevServer(compiler, Object.assign({
clientLogLevel: 'none',
historyApiFallback: {
@@ -84,16 +95,22 @@ module.exports = (api, options) => {
},
contentBase: api.resolve('public'),
watchContentBase: true,
https: useHttps,
hot: true,
quiet: true,
compress: true,
publicPath: webpackConfig.output.publicPath,
// TODO use custom overlay w/ open-in-editor
overlay: { warnings: false, errors: true },
// TODO handle proxy
proxy: {}
}, projectDevServerOptions))
publicPath: webpackConfig.output.publicPath
}, projectDevServerOptions, {
https: useHttps,
proxy: proxySettings,
before (app) {
// overlay
app.use(overlayMiddleware())
// allow other plugins to register middlewares, e.g. PWA
api.service.devServerConfigFns.forEach(fn => fn(app))
// apply in project middlewares
projectDevServerOptions.before && projectDevServerOptions.before(app)
}
}))
server.listen(port, host, err => {
if (err) {
@@ -0,0 +1,200 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file at
* https://github.com/facebookincubator/create-react-app/blob/master/LICENSE
*/
const fs = require('fs')
const url = require('url')
const path = require('path')
const chalk = require('chalk')
const address = require('address')
module.exports = function prepareProxy (proxy, appPublicFolder) {
// `proxy` lets you specify alternate servers for specific requests.
// It can either be a string or an object conforming to the Webpack dev server proxy configuration
// https://webpack.github.io/docs/webpack-dev-server.html
if (!proxy) {
return undefined
}
if (typeof proxy !== 'object' && typeof proxy !== 'string') {
console.log(
chalk.red(
'When specified, "proxy" in package.json must be a string or an object.'
)
)
console.log(
chalk.red('Instead, the type of "proxy" was "' + typeof proxy + '".')
)
console.log(
chalk.red(
'Either remove "proxy" from package.json, or make it an object.'
)
)
process.exit(1)
}
// Otherwise, if proxy is specified, we will let it handle any request except for files in the public folder.
function mayProxy (pathname) {
const maybePublicPath = path.resolve(appPublicFolder, pathname.slice(1))
return !fs.existsSync(maybePublicPath)
}
// Support proxy as a string for those who are using the simple proxy option
if (typeof proxy === 'string') {
if (!/^http(s)?:\/\//.test(proxy)) {
console.log(
chalk.red(
'When "proxy" is specified in package.json it must start with either http:// or https://'
)
)
process.exit(1)
}
let target
if (process.platform === 'win32') {
target = resolveLoopback(proxy)
} else {
target = proxy
}
return [
{
target,
logLevel: 'silent',
// For single page apps, we generally want to fallback to /index.html.
// However we also want to respect `proxy` for API calls.
// So if `proxy` is specified as a string, we need to decide which fallback to use.
// We use a heuristic: if request `accept`s text/html, we pick /index.html.
// Modern browsers include text/html into `accept` header when navigating.
// However API calls like `fetch()` wont generally accept text/html.
// If this heuristic doesnt work well for you, use a custom `proxy` object.
context: function (pathname, req) {
return (
mayProxy(pathname) &&
req.headers.accept &&
req.headers.accept.indexOf('text/html') === -1
)
},
onProxyReq: proxyReq => {
// Browers may send Origin headers even with same-origin
// requests. To prevent CORS issues, we have to change
// the Origin to match the target URL.
if (proxyReq.getHeader('origin')) {
proxyReq.setHeader('origin', target)
}
},
onError: onProxyError(target),
secure: false,
changeOrigin: true,
ws: true,
xfwd: true
}
]
}
// Otherwise, proxy is an object so create an array of proxies to pass to webpackDevServer
return Object.keys(proxy).map(function (context) {
if (!proxy[context].hasOwnProperty('target')) {
console.log(
chalk.red(
'When `proxy` in package.json is as an object, each `context` object must have a ' +
'`target` property specified as a url string'
)
)
process.exit(1)
}
let target
if (process.platform === 'win32') {
target = resolveLoopback(proxy[context].target)
} else {
target = proxy[context].target
}
return Object.assign({}, proxy[context], {
context: function (pathname) {
return mayProxy(pathname) && pathname.match(context)
},
onProxyReq: proxyReq => {
// Browers may send Origin headers even with same-origin
// requests. To prevent CORS issues, we have to change
// the Origin to match the target URL.
if (proxyReq.getHeader('origin')) {
proxyReq.setHeader('origin', target)
}
},
target,
onError: onProxyError(target)
})
})
}
function resolveLoopback (proxy) {
const o = url.parse(proxy)
o.host = undefined
if (o.hostname !== 'localhost') {
return proxy
}
// Unfortunately, many languages (unlike node) do not yet support IPv6.
// This means even though localhost resolves to ::1, the application
// must fall back to IPv4 (on 127.0.0.1).
// We can re-enable this in a few years.
/* try {
o.hostname = address.ipv6() ? '::1' : '127.0.0.1';
} catch (_ignored) {
o.hostname = '127.0.0.1';
}*/
try {
// Check if we're on a network; if we are, chances are we can resolve
// localhost. Otherwise, we can just be safe and assume localhost is
// IPv4 for maximum compatibility.
if (!address.ip()) {
o.hostname = '127.0.0.1'
}
} catch (_ignored) {
o.hostname = '127.0.0.1'
}
return url.format(o)
}
// We need to provide a custom onError function for httpProxyMiddleware.
// It allows us to log custom error messages on the console.
function onProxyError (proxy) {
return (err, req, res) => {
const host = req.headers && req.headers.host
console.log(
chalk.red('Proxy error:') +
' Could not proxy request ' +
chalk.cyan(req.url) +
' from ' +
chalk.cyan(host) +
' to ' +
chalk.cyan(proxy) +
'.'
)
console.log(
'See https://nodejs.org/api/errors.html#errors_common_system_errors for more information (' +
chalk.cyan(err.code) +
').'
)
console.log()
// And immediately send the proper error response to the client.
// Otherwise, the request will eventually timeout with ERR_EMPTY_RESPONSE on the client side.
if (res.writeHead && !res.headersSent) {
res.writeHead(500)
}
res.end(
'Proxy error: Could not proxy request ' +
req.url +
' from ' +
host +
' to ' +
proxy +
' (' +
err.code +
').'
)
}
}
+1
View File
@@ -21,6 +21,7 @@
},
"homepage": "https://github.com/vuejs/vue-cli/packages/@vue/cli-service#readme",
"dependencies": {
"@vue/cli-overlay": "^1.0.0",
"address": "^1.0.3",
"chalk": "^2.3.0",
"cross-spawn": "^5.1.0",
+22 -1
View File
@@ -192,6 +192,10 @@ array-equal@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93"
array-filter@~0.0.0:
version "0.0.1"
resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-0.0.1.tgz#7da8cf2e26628ed732803581fd21f67cacd2eeec"
array-find-index@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1"
@@ -215,6 +219,14 @@ array-includes@^3.0.3:
define-properties "^1.1.2"
es-abstract "^1.7.0"
array-map@~0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/array-map/-/array-map-0.0.0.tgz#88a2bab73d1cf7bcd5c1b118a003f66f665fa662"
array-reduce@~0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/array-reduce/-/array-reduce-0.0.0.tgz#173899d3ffd1c7d9383e4479525dbe278cab5f2b"
array-union@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39"
@@ -6220,6 +6232,15 @@ shebang-regex@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
shell-quote@^1.6.1:
version "1.6.1"
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.6.1.tgz#f4781949cce402697127430ea3b3c5476f481767"
dependencies:
array-filter "~0.0.0"
array-map "~0.0.0"
array-reduce "~0.0.0"
jsonify "~0.0.0"
shellwords@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
@@ -7032,7 +7053,7 @@ vue-template-es2015-compiler@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.6.0.tgz#dc42697133302ce3017524356a6c61b7b69b4a18"
vue-test-utils@^1.0.0:
vue-test-utils@^1.0.0-beta.9:
version "1.0.0-beta.9"
resolved "https://registry.yarnpkg.com/vue-test-utils/-/vue-test-utils-1.0.0-beta.9.tgz#bb67c01e2386f85c3ffbceae460b6e785eb7f81a"
dependencies: