-
Notifications
You must be signed in to change notification settings - Fork 54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add major features from webpack-dev-server #55
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,9 @@ | ||
import { readFile } from 'fs' | ||
import Express from 'express' | ||
import killable from 'killable' | ||
import compress from 'compression' | ||
import serveIndex from 'serve-index' | ||
import historyApiFallback from 'connect-history-api-fallback' | ||
import { createProxyMiddleware } from 'http-proxy-middleware' | ||
import { createServer as createHttpsServer } from 'https' | ||
import { createServer } from 'http' | ||
import { resolve } from 'path' | ||
|
@@ -7,6 +12,7 @@ import mime from 'mime' | |
import opener from 'opener' | ||
|
||
let server | ||
let app | ||
|
||
/** | ||
* Serve your rolled up bundle like webpack-dev-server | ||
|
@@ -17,61 +23,259 @@ function serve (options = { contentBase: '' }) { | |
options = { contentBase: options } | ||
} | ||
options.contentBase = Array.isArray(options.contentBase) ? options.contentBase : [options.contentBase || ''] | ||
options.contentBasePublicPath = options.contentBasePublicPath || '/' | ||
options.port = options.port || 10001 | ||
options.headers = options.headers || {} | ||
options.https = options.https || false | ||
options.openPage = options.openPage || '' | ||
options.compress = !!options.compress | ||
options.serveIndex = options.serveIndex || (options.serveIndex === undefined) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These new options should be documented in README.md There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep, obviously. And some tests added. |
||
mime.default_type = 'text/plain' | ||
|
||
const requestListener = (request, response) => { | ||
// Remove querystring | ||
const urlPath = decodeURI(request.url.split('?')[0]) | ||
function setupProxy () { | ||
/** | ||
* Assume a proxy configuration specified as: | ||
* proxy: { | ||
* 'context': { options } | ||
* } | ||
* OR | ||
* proxy: { | ||
* 'context': 'target' | ||
* } | ||
*/ | ||
if (!Array.isArray(options.proxy)) { | ||
if (Object.prototype.hasOwnProperty.call(options.proxy, 'target')) { | ||
options.proxy = [options.proxy] | ||
} else { | ||
options.proxy = Object.keys(options.proxy).map((context) => { | ||
let proxyOptions | ||
// For backwards compatibility reasons. | ||
const correctedContext = context | ||
.replace(/^\*$/, '**') | ||
.replace(/\/\*$/, '') | ||
|
||
Object.keys(options.headers).forEach((key) => { | ||
response.setHeader(key, options.headers[key]) | ||
}) | ||
if (typeof options.proxy[context] === 'string') { | ||
proxyOptions = { | ||
context: correctedContext, | ||
target: options.proxy[context] | ||
} | ||
} else { | ||
proxyOptions = Object.assign({}, options.proxy[context]) | ||
proxyOptions.context = correctedContext | ||
} | ||
|
||
proxyOptions.logLevel = proxyOptions.logLevel || 'warn' | ||
|
||
readFileFromContentBase(options.contentBase, urlPath, function (error, content, filePath) { | ||
if (!error) { | ||
return found(response, filePath, content) | ||
return proxyOptions | ||
}) | ||
} | ||
if (error.code !== 'ENOENT') { | ||
response.writeHead(500) | ||
response.end('500 Internal Server Error' + | ||
'\n\n' + filePath + | ||
'\n\n' + Object.values(error).join('\n') + | ||
'\n\n(rollup-plugin-serve)', 'utf-8') | ||
return | ||
} | ||
|
||
const getProxyMiddleware = (proxyConfig) => { | ||
const context = proxyConfig.context || proxyConfig.path | ||
|
||
// It is possible to use the `bypass` method without a `target`. | ||
// However, the proxy middleware has no use in this case, and will fail to instantiate. | ||
if (proxyConfig.target) { | ||
return createProxyMiddleware(context, proxyConfig) | ||
} | ||
if (options.historyApiFallback) { | ||
const fallbackPath = typeof options.historyApiFallback === 'string' ? options.historyApiFallback : '/index.html' | ||
readFileFromContentBase(options.contentBase, fallbackPath, function (error, content, filePath) { | ||
if (error) { | ||
notFound(response, filePath) | ||
} else { | ||
found(response, filePath, content) | ||
} | ||
/** | ||
* Assume a proxy configuration specified as: | ||
* proxy: [ | ||
* { | ||
* context: ..., | ||
* ...options... | ||
* }, | ||
* // or: | ||
* function() { | ||
* return { | ||
* context: ..., | ||
* ...options... | ||
* }; | ||
* } | ||
* ] | ||
*/ | ||
options.proxy.forEach((proxyConfigOrCallback) => { | ||
let proxyMiddleware | ||
|
||
let proxyConfig = | ||
typeof proxyConfigOrCallback === 'function' | ||
? proxyConfigOrCallback() | ||
: proxyConfigOrCallback | ||
|
||
proxyMiddleware = getProxyMiddleware(proxyConfig) | ||
|
||
function proxyHandle (req, res, next) { | ||
if (typeof proxyConfigOrCallback === 'function') { | ||
const newProxyConfig = proxyConfigOrCallback() | ||
|
||
if (newProxyConfig !== proxyConfig) { | ||
proxyConfig = newProxyConfig | ||
proxyMiddleware = getProxyMiddleware(proxyConfig) | ||
} | ||
}) | ||
} else { | ||
notFound(response, filePath) | ||
} | ||
|
||
// - Check if we have a bypass function defined | ||
// - In case the bypass function is defined we'll retrieve the | ||
// bypassUrl from it otherwise bypassUrl would be null | ||
const isByPassFuncDefined = typeof proxyConfig.bypass === 'function' | ||
const bypassUrl = isByPassFuncDefined | ||
? proxyConfig.bypass(req, res, proxyConfig) | ||
: null | ||
|
||
if (typeof bypassUrl === 'boolean') { | ||
// skip the proxy | ||
req.url = null | ||
next() | ||
} else if (typeof bypassUrl === 'string') { | ||
// byPass to that url | ||
req.url = bypassUrl | ||
next() | ||
} else if (proxyMiddleware) { | ||
return proxyMiddleware(req, res, next) | ||
} else { | ||
next() | ||
} | ||
} | ||
|
||
app.use(proxyHandle) | ||
// Also forward error requests to the proxy so it can handle them. | ||
// eslint-disable-next-line handle-callback-err | ||
app.use((error, req, res, next) => proxyHandle(req, res, next)) | ||
}) | ||
} | ||
|
||
// release previous server instance if rollup is reloading configuration in watch mode | ||
if (server) { | ||
server.close() | ||
server.kill() | ||
} else { | ||
closeServerOnTermination() | ||
} | ||
|
||
app = new Express() | ||
|
||
// Implement webpack-dev-server features | ||
const features = { | ||
compress: () => { | ||
if (options.compress) { | ||
app.use(compress()) | ||
} | ||
}, | ||
proxy: () => { | ||
if (options.proxy) { | ||
setupProxy() | ||
} | ||
}, | ||
historyApiFallback: () => { | ||
if (options.historyApiFallback) { | ||
const fallback = | ||
typeof options.historyApiFallback === 'object' | ||
? options.historyApiFallback | ||
: typeof options.historyApiFallback === 'string' | ||
? { index: options.historyApiFallback, disableDotRule: true } : null | ||
|
||
app.use(historyApiFallback(fallback)) | ||
} | ||
}, | ||
contentBaseFiles: () => { | ||
if (Array.isArray(options.contentBase)) { | ||
options.contentBase.forEach((item) => { | ||
app.use(options.contentBasePublicPath, Express.static(item)) | ||
}) | ||
} else { | ||
app.use( | ||
options.contentBasePublicPath, | ||
Express.static(options.contentBase, options.staticOptions) | ||
) | ||
} | ||
}, | ||
contentBaseIndex: () => { | ||
if (options.contentBase && options.serveIndex) { | ||
const getHandler = item => function indexHandler (req, res, next) { | ||
// serve-index doesn't fallthrough non-get/head request to next middleware | ||
if (req.method !== 'GET' && req.method !== 'HEAD') { | ||
return next() | ||
} | ||
|
||
serveIndex(item)(req, res, next) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The interface of This is possible now, using the defaults of serve({
open: false,
port: 8080,
contentBase: __dirname,
serveIndex: true
}) How about letting us customise the options of serve({
open: false,
port: 8080,
contentBase: __dirname,
serveIndex: { icons: true }
}) It'd mean this code change: - serveIndex(item)(req, res, next)
+ serveIndex(item, options.serveIndex)(req, res, next) |
||
} | ||
if (Array.isArray(options.contentBase)) { | ||
options.contentBase.forEach((item) => { | ||
app.use(options.contentBasePublicPath, getHandler(item)) | ||
}) | ||
} else { | ||
app.use(options.contentBasePublicPath, getHandler(options.contentBase)) | ||
} | ||
} | ||
}, | ||
before: () => { | ||
if (typeof options.before === 'function') { | ||
options.before(app) | ||
} | ||
}, | ||
after: () => { | ||
if (typeof options.after === 'function') { | ||
options.after(app) | ||
} | ||
}, | ||
headers: () => { | ||
app.all('*', function headersHandler (req, res, next) { | ||
if (options.headers) { | ||
for (const name in options.headers) { | ||
res.setHeader(name, options.headers[name]) | ||
} | ||
} | ||
next() | ||
}) | ||
} | ||
} | ||
|
||
const runnableFeatures = [] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How much of this code is copied from webpack-dev-server? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Some of the complex features. Like proxy and feature order. |
||
|
||
if (options.compress) { | ||
runnableFeatures.push('compress') | ||
} | ||
|
||
runnableFeatures.push('before', 'headers') | ||
|
||
if (options.proxy) { | ||
runnableFeatures.push('proxy') | ||
} | ||
|
||
if (options.contentBase !== false) { | ||
runnableFeatures.push('contentBaseFiles') | ||
} | ||
|
||
if (options.historyApiFallback) { | ||
runnableFeatures.push('historyApiFallback') | ||
|
||
if (options.contentBase !== false) { | ||
runnableFeatures.push('contentBaseFiles') | ||
} | ||
} | ||
|
||
if (options.contentBase && options.serveIndex) { | ||
runnableFeatures.push('contentBaseIndex') | ||
} | ||
|
||
if (options.after) { | ||
runnableFeatures.push('after') | ||
} | ||
|
||
(options.features || runnableFeatures).forEach((feature) => { | ||
features[feature]() | ||
}) | ||
|
||
// If HTTPS options are available, create an HTTPS server | ||
if (options.https) { | ||
server = createHttpsServer(options.https, requestListener).listen(options.port, options.host) | ||
server = createHttpsServer(options.https, app).listen(options.port, options.host) | ||
} else { | ||
server = createServer(requestListener).listen(options.port, options.host) | ||
server = createServer(app).listen(options.port, options.host) | ||
} | ||
|
||
killable(server) | ||
|
||
let running = options.verbose === false | ||
|
||
return { | ||
|
@@ -99,47 +303,16 @@ function serve (options = { contentBase: '' }) { | |
} | ||
} | ||
|
||
function readFileFromContentBase (contentBase, urlPath, callback) { | ||
let filePath = resolve(contentBase[0] || '.', '.' + urlPath) | ||
|
||
// Load index.html in directories | ||
if (urlPath.endsWith('/')) { | ||
filePath = resolve(filePath, 'index.html') | ||
} | ||
|
||
readFile(filePath, (error, content) => { | ||
if (error && contentBase.length > 1) { | ||
// Try to read from next contentBase | ||
readFileFromContentBase(contentBase.slice(1), urlPath, callback) | ||
} else { | ||
// We know enough | ||
callback(error, content, filePath) | ||
} | ||
}) | ||
} | ||
|
||
function notFound (response, filePath) { | ||
response.writeHead(404) | ||
response.end('404 Not Found' + | ||
'\n\n' + filePath + | ||
'\n\n(rollup-plugin-serve)', 'utf-8') | ||
} | ||
|
||
function found (response, filePath, content) { | ||
response.writeHead(200, { 'Content-Type': mime.getType(filePath) }) | ||
response.end(content, 'utf-8') | ||
} | ||
|
||
function green (text) { | ||
return '\u001b[1m\u001b[32m' + text + '\u001b[39m\u001b[22m' | ||
} | ||
|
||
function closeServerOnTermination() { | ||
function closeServerOnTermination () { | ||
const terminationSignals = ['SIGINT', 'SIGTERM', 'SIGQUIT', 'SIGHUP'] | ||
terminationSignals.forEach(signal => { | ||
process.on(signal, () => { | ||
if (server) { | ||
server.close() | ||
server.kill() | ||
process.exit() | ||
} | ||
}) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you think about moving all of these to devDependencies and bundling them in the plugin with rollup?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems ok.