preact-material-components
Version:
preact wrapper for "Material Components for the web"
217 lines (176 loc) • 6.4 kB
JavaScript
// Native
const path = require('path')
const { parse, format } = require('url')
// Packages
const micro = require('micro')
const auth = require('basic-auth')
const { red } = require('chalk')
const fs = require('fs-extra')
const pathType = require('path-type')
const mime = require('mime-types')
const stream = require('send')
const { coroutine } = require('bluebird')
const isPathInside = require('path-is-inside')
// Utilities
const renderDirectory = require('./render')
module.exports = coroutine(function*(req, res, flags, current, ignoredFiles) {
const headers = {}
if (flags.cors) {
headers['Access-Control-Allow-Origin'] = '*'
headers['Access-Control-Allow-Headers'] =
'Origin, X-Requested-With, Content-Type, Accept, Range'
}
for (const header in headers) {
if (!{}.hasOwnProperty.call(headers, header)) {
continue
}
res.setHeader(header, headers[header])
}
if (flags.auth) {
const credentials = auth(req)
if (!process.env.SERVE_USER || !process.env.SERVE_PASSWORD) {
const error =
'The environment variables "SERVE_USER" ' +
'and/or "SERVE_PASSWORD" are missing!'
console.error(red(error))
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1)
}
if (
!credentials ||
credentials.name !== process.env.SERVE_USER ||
credentials.pass !== process.env.SERVE_PASSWORD
) {
res.statusCode = 401
res.setHeader('WWW-Authenticate', 'Basic realm="User Visible Realm"')
return micro.send(res, 401, 'Access Denied')
}
}
const { pathname } = parse(req.url)
const { ASSET_DIR } = process.env
let related = path.parse(path.join(current, pathname))
let assetRequest = false
if (path.basename(related.dir) === ASSET_DIR) {
assetRequest = true
related = decodeURIComponent(path.join(__dirname, '/../assets/styles.css'))
} else {
related = decodeURIComponent(path.format(related))
const relatedResolved = path.resolve(related)
const relatedCurrent = path.resolve(current)
const isSame = relatedResolved === relatedCurrent
if (!isSame && !isPathInside(relatedResolved, relatedCurrent)) {
return micro.send(res, 400, 'Bad Request')
}
}
let notFoundResponse = 'Not Found'
try {
const custom404Path = path.join(current, '/404.html')
notFoundResponse = yield fs.readFile(custom404Path, 'utf-8')
} catch (err) {}
// Don't allow rendering ignored files
const ignored = !ignoredFiles.every(item => {
return !pathname.includes(item)
})
if (ignored || (!assetRequest && related.indexOf(current) !== 0)) {
return micro.send(res, 404, notFoundResponse)
}
const relatedExists = yield fs.exists(related)
if (!relatedExists && !flags.single) {
return micro.send(res, 404, notFoundResponse)
}
const streamOptions = {}
const cacheValue = parseInt(flags.cache, 10)
if (cacheValue > 0) {
streamOptions.maxAge = flags.cache
} else if (cacheValue === 0) {
// Disable the cache control by `send`, as there's no support for `no-cache`.
// Set header manually.
streamOptions.cacheControl = false
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
} else if (flags.single) {
// Cache assets of single page applications for a day.
// Later in the code, we'll define that `index.html` never
// gets cached!
streamOptions.maxAge = 86400000
}
// Check if directory
if (relatedExists && (yield pathType.dir(related))) {
// Normalize path to trailing slash
// Otherwise problems like #70 will occur
const url = parse(req.url)
if (url.pathname.substr(-1) !== '/') {
url.pathname += '/'
const newPath = format(url)
res.writeHead(302, {
Location: newPath
})
res.end()
return
}
let indexPath = path.join(related, '/index.html')
res.setHeader('Content-Type', mime.contentType(path.extname(indexPath)))
if (!(yield fs.exists(indexPath))) {
// Try to render the current directory's content
const port = flags.port || req.socket.localPort
const renderedDir = yield renderDirectory(
port,
current,
related,
ignoredFiles
)
// If it works, send the directory listing to the user
// If is treeless, stepover
if (renderedDir && !flags.treeless) {
return micro.send(res, 200, renderedDir)
}
// And if it doesn't, see if it's a single page application
// If that's not true either, send an error
if (!flags.single) {
return micro.send(res, 404, notFoundResponse)
}
// But IF IT IS true, then this is a hit to a directory with no `index.html`
// Load the SPA's root index file
indexPath = path.join(current, '/index.html')
}
if (flags.single && indexPath === path.join(current, '/index.html')) {
// Don't cache the `index.html` file for single page applications
streamOptions.maxAge = 0
}
return stream(req, indexPath, streamOptions).pipe(res)
}
// Serve `index.html` for single page applications if:
// - The path does not match any file or directory OR
// - The path matches exactly `/index.html`
if (
flags.single &&
(!relatedExists || related === path.join(current, '/index.html'))
) {
// Don't cache the `index.html` file for single page applications
streamOptions.maxAge = 0
// Load `index.html` and send it to the client
const indexPath = path.join(current, '/index.html')
return stream(req, indexPath, streamOptions).pipe(res)
}
// TODO remove the change below when https://github.com/zeit/serve/issues/303 is fixed.
//
// Details:
// Register the mime type for `.wasm` files. This change is currently not
// possible on upstream because of an unrelated issue. Once `mime` is
// updated, this line can be removed.
// https://github.com/pillarjs/send/pull/154
stream.mime.types.wasm = 'application/wasm'
stream.mime.extensions['application/wasm'] = 'wasm'
// Serve files without a mime type as html for SPA, and text for non SPA
// eslint-disable-next-line camelcase
stream.mime.default_type = flags.single ? 'text/html' : 'text/plain'
return stream(
req,
related,
Object.assign(
{
dotfiles: 'allow'
},
streamOptions
)
).pipe(res)
})