Skip to content

Commit

Permalink
add major features from webpack-dev-server
Browse files Browse the repository at this point in the history
- compress
- proxy
- before
- after
- historyApiFallback (object format)
- serveIndex
- headers
  • Loading branch information
pkit committed Feb 21, 2020
1 parent 0e17e03 commit e87e3dc
Show file tree
Hide file tree
Showing 3 changed files with 746 additions and 68 deletions.
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,14 @@
"dist"
],
"dependencies": {
"compression": "^1.7.4",
"connect-history-api-fallback": "^1.6.0",
"express": "^4.17.1",
"http-proxy-middleware": "^1.0.0",
"killable": "^1.0.1",
"mime": ">=2.0.3",
"opener": "1"
"opener": "1",
"serve-index": "^1.9.1"
},
"devDependencies": {
"rollup": "1",
Expand Down
299 changes: 236 additions & 63 deletions src/index.js
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'
Expand All @@ -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
Expand All @@ -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)
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)
}
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 = []

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 {
Expand Down Expand Up @@ -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()
}
})
Expand Down

0 comments on commit e87e3dc

Please sign in to comment.