jetpack
Version:
Jetpack wraps webpack and nodemon to give you the best development workflow.
168 lines (145 loc) • 4.94 kB
JavaScript
/**
* handle node req/res
* and respond with client side app in both dev and prd!
* in dev – proxy to the jetpack dev server
* in prd – serve the static assets from dist
* handle all that jazz so you don't have to
*/
const { existsSync } = require('fs')
const fs = require('fs/promises')
const path = require('path')
const send = require('send')
const http = require('http')
const chalk = require('picocolors')
const parseUrl = require('parseurl')
const browsers = require('./browsers')
const getOptions = require('./options')
const options = getOptions()
const env = process.env.NODE_ENV || 'development'
module.exports =
env === 'development'
? createProxy(`http://localhost:${options.port}`)
: createServe(path.join(options.dir, options.dist))
function createProxy(target) {
const errorHtmlPath = path.join(__dirname, 'html', 'error-not-running.html')
const errorHtmlPromise = fs.readFile(errorHtmlPath, 'utf8').catch(() => null)
return function proxy(req, res, next) {
try {
const parsedTarget = new URL(target)
const parsedReq = parseUrl(req)
const reqOpt = {
host: parsedTarget.hostname,
port: parsedTarget.port,
headers: req.headers,
method: req.method,
path: parsedReq.path,
params: req.params,
session: req.session
}
const sendProxyError = (statusCode, headers, body) => {
if (res.headersSent || res.writableEnded) {
return
}
res.writeHead(statusCode, headers)
res.end(body)
}
const proxyReq = http.request(reqOpt, function (proxyRes) {
if (res.headersSent || res.writableEnded) {
proxyRes.resume()
return
}
res.writeHead(proxyRes.statusCode, proxyRes.headers)
proxyRes.pipe(res)
})
proxyReq.setTimeout(30000, function () {
proxyReq.destroy(new Error('Request timeout'))
})
proxyReq.on('error', async function (err) {
if (err.code === 'ECONNREFUSED') {
const msg = `[jetpack] Failed to connect to the jetpack dev server. Start it by running \`jetpack\` in your project.`
console.error(chalk.red(msg))
const acceptHeader = req.headers['accept'] || ''
if (acceptHeader.includes('text/html')) {
const html = await errorHtmlPromise
if (html) {
sendProxyError(502, { 'Content-Type': 'text/html' }, html)
} else {
sendProxyError(
502,
{ 'Content-Type': 'text/html' },
'<!DOCTYPE html><h1>jetpack not running</h1><p>Failed to connect to the jetpack dev server.</p>'
)
}
} else {
sendProxyError(502, { 'Content-Type': 'application/json' }, JSON.stringify({ error: msg }))
}
} else if (err.message === 'Request timeout') {
console.error(chalk.red('[jetpack] Proxy request timed out'))
sendProxyError(
504,
{ 'Content-Type': 'application/json' },
JSON.stringify({ error: 'Proxy request timed out' })
)
} else {
console.log(err)
sendProxyError(502, { 'Content-Type': 'application/json' }, JSON.stringify({ error: err.stack }))
}
})
req.pipe(proxyReq)
} catch (err) {
next(err)
}
}
}
function createServe(root) {
const modernBundleExists = existsSync(path.join(root, 'index.html'))
const legacyBundleExists = existsSync(path.join(root, 'index.legacy.html'))
function getIndex(userAgent, modernBrowserRegex) {
if (!legacyBundleExists && !modernBundleExists) {
return null
}
if (!legacyBundleExists) {
return '/index.html'
}
if (!modernBundleExists) {
return '/index.legacy.html'
}
if (userAgent && modernBrowserRegex.test(userAgent)) {
return '/index.html'
}
return '/index.legacy.html'
}
return async function serve(req, res, next) {
if (req.method !== 'GET' && req.method !== 'HEAD') return next()
let pathname = parseUrl(req).pathname
let index = false
try {
const stats = await fs.stat(path.join(root, pathname))
index = !stats.isFile()
} catch (err) {
if (err.code === 'ENOENT') {
index = true
}
}
if (index) {
pathname = getIndex(req.headers['user-agent'], await getModernBrowserRegex())
}
if (pathname) {
const stream = send(req, pathname, { root })
stream.on('directory', () => next())
stream.on('error', (err) => next(err))
stream.pipe(res)
} else {
res.send('No bundle found')
}
}
}
let modernBrowserRegex
async function getModernBrowserRegex() {
if (!modernBrowserRegex) {
modernBrowserRegex = browsers.regex({ modern: true }).then((re) => {
modernBrowserRegex = re
})
}
return modernBrowserRegex
}