From d3d892f77286a2c263ed41c733919e0e2e745ce2 Mon Sep 17 00:00:00 2001 From: Doru Moisa Date: Wed, 15 Aug 2018 21:06:22 +0300 Subject: [PATCH 1/2] Added `indexFiles` and `fileExtensions` options for allowing more fine-grained config over the served files, focused on esm loading in the browser (importing folders or extensionless files) but not limited to that. --- README.md | 6 ++++ src/index.js | 91 ++++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 76 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index cc2bf6b..ce52989 100644 --- a/README.md +++ b/README.md @@ -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, diff --git a/src/index.js b/src/index.js index cf1eef4..d6e6d7c 100644 --- a/src/index.js +++ b/src/index.js @@ -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) => { @@ -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) } @@ -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 { @@ -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.lengt - 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) { From 0a2f2afc8aff309a165c50e9602d5233b79c6c39 Mon Sep 17 00:00:00 2001 From: Doru Moisa Date: Wed, 15 Aug 2018 23:17:45 +0300 Subject: [PATCH 2/2] Fixed typo. --- src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index d6e6d7c..5d62ddc 100644 --- a/src/index.js +++ b/src/index.js @@ -131,7 +131,7 @@ function readFileFromContentBase (options, urlPath, response, callback) { } // when still not found, try all the configured folder indexes - if (fileIdx < candidateFiles.lengt - 1) { + if (fileIdx < candidateFiles.length - 1) { fileIdx += 1 return tryFile() } else {