Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ serve({
// Multiple folders to serve from
contentBase: ['dist', 'static'],

// Directory index files
indexFiles: ['index.html'],

// File extensions to try if no file
fileExtensions: [],

// Set to true to return index.html instead of 404
historyApiFallback: false,

Expand Down
91 changes: 70 additions & 21 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,24 @@ import { resolve } from 'path'
import mime from 'mime'
import opener from 'opener'

export default function serve (options = { contentBase: '' }) {
export default function serve (options) {
if (Array.isArray(options) || typeof options === 'string') {
options = { contentBase: options }
}

options = Object.assign({
contentBase: '',
indexFiles: 'index.html',
fileExtensions: [],
host: 'localhost',
port: 10001,
headers: {},
https: false
}, options)

options.contentBase = Array.isArray(options.contentBase) ? options.contentBase : [options.contentBase]
options.host = options.host || 'localhost'
options.port = options.port || 10001
options.headers = options.headers || {}
options.https = options.https || false
options.indexFiles = Array.isArray(options.indexFiles) ? options.indexFiles : [options.indexFiles]
options.fileExtensions = Array.isArray(options.fileExtensions) ? options.fileExtensions : [options.fileExtensions]
mime.default_type = 'text/plain'

const requestListener = (request, response) => {
Expand All @@ -25,7 +34,7 @@ export default function serve (options = { contentBase: '' }) {
response.setHeader(key, options.headers[key])
})

readFileFromContentBase(options.contentBase, urlPath, function (error, content, filePath) {
readFileFromContentBase(options, urlPath, response, function (error, content, filePath) {
if (!error) {
return found(response, filePath, content)
}
Expand All @@ -49,7 +58,7 @@ export default function serve (options = { contentBase: '' }) {
}
})
} else if (options.historyApiFallback) {
readFileFromContentBase(options.contentBase, '/index.html', function (error, content, filePath) {
readFileFromContentBase(options, '/index.html', response, function (error, content, filePath) {
if (error) {
notFound(response, filePath)
} else {
Expand Down Expand Up @@ -95,23 +104,63 @@ export default function serve (options = { contentBase: '' }) {
}
}

function readFileFromContentBase (contentBase, urlPath, callback) {
let filePath = resolve(contentBase[0] || '.', '.' + urlPath)
function readFileFromContentBase (options, urlPath, response, callback) {
const contentBase = options.contentBase
const candidateFiles = [''].concat(options.indexFiles)
const fileExtensions = [''].concat(options.fileExtensions)

let extIdx = 0
let baseIdx = 0
let fileIdx = 0

function tryFile () {
const crtExt = fileExtensions[extIdx]
const crtBase = contentBase[baseIdx]
const crtFile = candidateFiles[fileIdx]

const filePath = resolve(crtBase || '.', '.' + urlPath, crtFile) + crtExt

readFile(filePath, (error, content) => {
if (error) {
// when not found, try all the configured file extensions
if (extIdx < fileExtensions.length - 1) {
extIdx += 1
return tryFile()
} else {
extIdx = 0
}

// Load index.html in directories
if (urlPath.endsWith('/')) {
filePath = resolve(filePath, 'index.html')
}
// when still not found, try all the configured folder indexes
if (fileIdx < candidateFiles.length - 1) {
fileIdx += 1
return tryFile()
} else {
fileIdx = 0
}

// when still not found, try all the configured content bases
if (baseIdx < contentBase.length - 1) {
baseIdx += 1
return tryFile()
}
}

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
// when found a file match, but it is one folder deeper, redirect (needed for esm loding)
if (!error && crtFile !== '') {
const location = urlPath.replace(/\/+$/, '') + '/' + crtFile
return redirect(response, location)
}

// all done.
callback(error, content, filePath)
}
})
})
}
tryFile()
}

function redirect (response, location) {
response.writeHead(307, { 'Location': location })
response.end()
}

function notFound (response, filePath) {
Expand Down