diff --git a/.gitignore b/.gitignore index cfabde7..f9a7562 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,6 @@ node_modules .DS_Store # Build files -dist/* +#dist/* !dist/favicon.ico test/dest.js diff --git a/dist/index.cjs.js b/dist/index.cjs.js new file mode 100644 index 0000000..b9534d8 --- /dev/null +++ b/dist/index.cjs.js @@ -0,0 +1,199 @@ +'use strict'; + +function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } + +var fs = require('fs'); +var https = require('https'); +var http = require('http'); +var path = require('path'); +var mime = _interopDefault(require('mime')); +var opener = _interopDefault(require('opener')); + +var server; + +/** + * Serve your rolled up bundle like webpack-dev-server + * @param {ServeOptions|string|string[]} options + */ +function serve (options) { + if ( options === void 0 ) options = { contentBase: '' }; + + if (Array.isArray(options) || typeof options === 'string') { + options = { contentBase: 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.openPage = options.openPage || ''; + mime.default_type = 'text/plain'; + + var requestListener = function (request, response) { + // Remove querystring + var urlPath = decodeURI(request.url.split('?')[0]); + + Object.keys(options.headers).forEach(function (key) { + response.setHeader(key, options.headers[key]); + }); + + // Get range request header, For example: `range: bytes=0-5`` + var range = request.headers['range']; + var rangeSta = 0; + var rangeEnd = 0; + if (range) { + var ref = range.match(/(\d*)-(\d*)/); + var start = ref[1]; + var end = ref[2]; + rangeSta = +start || 0; + rangeEnd = +end || 0; + } + + readFileFromContentBase(options.contentBase, urlPath, function (error, content, filePath) { + if (!error) { + return found(response, filePath, content, rangeSta, rangeEnd) + } + 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 + } + if (options.historyApiFallback) { + var 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, rangeSta, rangeEnd); + } + }); + } else { + notFound(response, filePath); + } + }); + }; + + // release previous server instance if rollup is reloading configuration in watch mode + if (server) { + server.close(); + } + + // If HTTPS options are available, create an HTTPS server + if (options.https) { + server = https.createServer(options.https, requestListener).listen(options.port, options.host); + } else { + server = http.createServer(requestListener).listen(options.port, options.host); + } + + closeServerOnTermination(server); + + var running = options.verbose === false; + + return { + name: 'serve', + generateBundle: function generateBundle () { + if (!running) { + running = true; + + // Log which url to visit + var url = (options.https ? 'https' : 'http') + '://' + options.host + ':' + options.port; + options.contentBase.forEach(function (base) { + console.log(green(url) + ' -> ' + path.resolve(base)); + }); + + // Open browser + if (options.open) { + opener(url + options.openPage); + } + } + } + } +} + +function readFileFromContentBase (contentBase, urlPath, callback) { + var filePath = path.resolve(contentBase[0] || '.', '.' + urlPath); + + // Load index.html in directories + if (urlPath.endsWith('/')) { + filePath = path.resolve(filePath, 'index.html'); + } + + fs.readFile(filePath, function (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, rangeSta, rangeEnd) { + var headers = { + 'Content-Type': mime.getType(filePath) + }; + var statusCode = 200; + if (rangeSta !== 0 || rangeEnd !== 0) { + statusCode = 206; + var len = content.length; + var maxEnd = len - 1; + if (rangeEnd === 0) { + rangeEnd = maxEnd; + } + rangeEnd = Math.min(maxEnd, rangeEnd); + content = content.slice(rangeSta, rangeEnd); + headers['Accept-Ranges'] = 'bytes'; + headers['Content-Range'] = "bytes " + rangeSta + "-" + rangeEnd + "/" + len; + } + headers['Content-Length'] = content.length; + response.writeHead(statusCode, headers); + response.end(content, 'utf-8'); +} + +function green (text) { + return '\u001b[1m\u001b[32m' + text + '\u001b[39m\u001b[22m' +} + +function closeServerOnTermination (server) { + var terminationSignals = ['SIGINT', 'SIGTERM']; + terminationSignals.forEach(function (signal) { + process.on(signal, function () { + server.close(); + process.exit(); + }); + }); +} + +/** + * @typedef {Object} ServeOptions + * @property {boolean} [open=false] Launch in browser (default: `false`) + * @property {string} [openPage=''] Page to navigate to when opening the browser. Will not do anything if `open` is `false`. Remember to start with a slash e.g. `'/different/page'` + * @property {boolean} [verbose=true] Show server address in console (default: `true`) + * @property {string|string[]} [contentBase=''] Folder(s) to serve files from + * @property {string|boolean} [historyApiFallback] Path to fallback page. Set to `true` to return index.html (200) instead of error page (404) + * @property {string} [host='localhost'] Server host (default: `'localhost'`) + * @property {number} [port=10001] Server port (default: `10001`) + * @property {ServeOptionsHttps} [https=false] By default server will be served over HTTP (https: `false`). It can optionally be served over HTTPS + * @property {{[header:string]: string}} [headers] Set headers + */ + +/** + * @typedef {Object} ServeOptionsHttps + * @property {string|Buffer|Buffer[]|Object[]} key + * @property {string|Buffer|Array} cert + * @property {string|Buffer|Array} ca + * @see https.ServerOptions + */ + +module.exports = serve; diff --git a/dist/index.es.js b/dist/index.es.js new file mode 100644 index 0000000..ab14f5c --- /dev/null +++ b/dist/index.es.js @@ -0,0 +1,195 @@ +import { readFile } from 'fs'; +import { createServer } from 'https'; +import { createServer as createServer$1 } from 'http'; +import { resolve } from 'path'; +import mime from 'mime'; +import opener from 'opener'; + +var server; + +/** + * Serve your rolled up bundle like webpack-dev-server + * @param {ServeOptions|string|string[]} options + */ +function serve (options) { + if ( options === void 0 ) options = { contentBase: '' }; + + if (Array.isArray(options) || typeof options === 'string') { + options = { contentBase: 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.openPage = options.openPage || ''; + mime.default_type = 'text/plain'; + + var requestListener = function (request, response) { + // Remove querystring + var urlPath = decodeURI(request.url.split('?')[0]); + + Object.keys(options.headers).forEach(function (key) { + response.setHeader(key, options.headers[key]); + }); + + // Get range request header, For example: `range: bytes=0-5`` + var range = request.headers['range']; + var rangeSta = 0; + var rangeEnd = 0; + if (range) { + var ref = range.match(/(\d*)-(\d*)/); + var start = ref[1]; + var end = ref[2]; + rangeSta = +start || 0; + rangeEnd = +end || 0; + } + + readFileFromContentBase(options.contentBase, urlPath, function (error, content, filePath) { + if (!error) { + return found(response, filePath, content, rangeSta, rangeEnd) + } + 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 + } + if (options.historyApiFallback) { + var 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, rangeSta, rangeEnd); + } + }); + } else { + notFound(response, filePath); + } + }); + }; + + // release previous server instance if rollup is reloading configuration in watch mode + if (server) { + server.close(); + } + + // If HTTPS options are available, create an HTTPS server + if (options.https) { + server = createServer(options.https, requestListener).listen(options.port, options.host); + } else { + server = createServer$1(requestListener).listen(options.port, options.host); + } + + closeServerOnTermination(server); + + var running = options.verbose === false; + + return { + name: 'serve', + generateBundle: function generateBundle () { + if (!running) { + running = true; + + // Log which url to visit + var url = (options.https ? 'https' : 'http') + '://' + options.host + ':' + options.port; + options.contentBase.forEach(function (base) { + console.log(green(url) + ' -> ' + resolve(base)); + }); + + // Open browser + if (options.open) { + opener(url + options.openPage); + } + } + } + } +} + +function readFileFromContentBase (contentBase, urlPath, callback) { + var filePath = resolve(contentBase[0] || '.', '.' + urlPath); + + // Load index.html in directories + if (urlPath.endsWith('/')) { + filePath = resolve(filePath, 'index.html'); + } + + readFile(filePath, function (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, rangeSta, rangeEnd) { + var headers = { + 'Content-Type': mime.getType(filePath) + }; + var statusCode = 200; + if (rangeSta !== 0 || rangeEnd !== 0) { + statusCode = 206; + var len = content.length; + var maxEnd = len - 1; + if (rangeEnd === 0) { + rangeEnd = maxEnd; + } + rangeEnd = Math.min(maxEnd, rangeEnd); + content = content.slice(rangeSta, rangeEnd); + headers['Accept-Ranges'] = 'bytes'; + headers['Content-Range'] = "bytes " + rangeSta + "-" + rangeEnd + "/" + len; + } + headers['Content-Length'] = content.length; + response.writeHead(statusCode, headers); + response.end(content, 'utf-8'); +} + +function green (text) { + return '\u001b[1m\u001b[32m' + text + '\u001b[39m\u001b[22m' +} + +function closeServerOnTermination (server) { + var terminationSignals = ['SIGINT', 'SIGTERM']; + terminationSignals.forEach(function (signal) { + process.on(signal, function () { + server.close(); + process.exit(); + }); + }); +} + +/** + * @typedef {Object} ServeOptions + * @property {boolean} [open=false] Launch in browser (default: `false`) + * @property {string} [openPage=''] Page to navigate to when opening the browser. Will not do anything if `open` is `false`. Remember to start with a slash e.g. `'/different/page'` + * @property {boolean} [verbose=true] Show server address in console (default: `true`) + * @property {string|string[]} [contentBase=''] Folder(s) to serve files from + * @property {string|boolean} [historyApiFallback] Path to fallback page. Set to `true` to return index.html (200) instead of error page (404) + * @property {string} [host='localhost'] Server host (default: `'localhost'`) + * @property {number} [port=10001] Server port (default: `10001`) + * @property {ServeOptionsHttps} [https=false] By default server will be served over HTTP (https: `false`). It can optionally be served over HTTPS + * @property {{[header:string]: string}} [headers] Set headers + */ + +/** + * @typedef {Object} ServeOptionsHttps + * @property {string|Buffer|Buffer[]|Object[]} key + * @property {string|Buffer|Array} cert + * @property {string|Buffer|Array} ca + * @see https.ServerOptions + */ + +export default serve; diff --git a/package.json b/package.json index 03e7489..fbcfe17 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "rollup-plugin-serve", - "version": "1.0.0", + "name": "rollup-plugin-serve-range", + "version": "1.0.1", "description": "Serve your rolled up bundle", "main": "dist/index.cjs.js", "module": "dist/index.es.js", @@ -21,13 +21,13 @@ ], "license": "MIT", "author": "Thomas Ghysels ", - "homepage": "https://github.com/thgh/rollup-plugin-serve", + "homepage": "https://github.com/huzunjie/rollup-plugin-serve", "bugs": { - "url": "https://github.com/thgh/rollup-plugin-serve/issues" + "url": "https://github.com/huzunjie/rollup-plugin-serve/issues" }, "repository": { "type": "git", - "url": "https://github.com/thgh/rollup-plugin-serve" + "url": "https://github.com/huzunjie/rollup-plugin-serve" }, "files": [ "dist" diff --git a/src/index.js b/src/index.js index ec9ee3c..3be6667 100644 --- a/src/index.js +++ b/src/index.js @@ -32,9 +32,19 @@ function serve (options = { contentBase: '' }) { response.setHeader(key, options.headers[key]) }) + // Get range request header, For example: `range: bytes=0-5`` + const range = request.headers['range'] + let rangeSta = 0 + let rangeEnd = 0 + if (range) { + const [, start, end] = range.match(/(\d*)-(\d*)/) + rangeSta = +start || 0 + rangeEnd = +end || 0 + } + readFileFromContentBase(options.contentBase, urlPath, function (error, content, filePath) { if (!error) { - return found(response, filePath, content) + return found(response, filePath, content, rangeSta, rangeEnd) } if (error.code !== 'ENOENT') { response.writeHead(500) @@ -50,7 +60,7 @@ function serve (options = { contentBase: '' }) { if (error) { notFound(response, filePath) } else { - found(response, filePath, content) + found(response, filePath, content, rangeSta, rangeEnd) } }) } else { @@ -122,8 +132,25 @@ function notFound (response, filePath) { '\n\n(rollup-plugin-serve)', 'utf-8') } -function found (response, filePath, content) { - response.writeHead(200, { 'Content-Type': mime.getType(filePath) }) +function found (response, filePath, content, rangeSta, rangeEnd) { + const headers = { + 'Content-Type': mime.getType(filePath) + } + let statusCode = 200 + if (rangeSta !== 0 || rangeEnd !== 0) { + statusCode = 206 + const len = content.length + const maxEnd = len - 1; + if (rangeEnd === 0) { + rangeEnd = maxEnd + } + rangeEnd = Math.min(maxEnd, rangeEnd) + content = content.slice(rangeSta, rangeEnd) + headers['Accept-Ranges'] = 'bytes' + headers['Content-Range'] = `bytes ${rangeSta}-${rangeEnd}/${len}` + } + headers['Content-Length'] = content.length + response.writeHead(statusCode, headers) response.end(content, 'utf-8') }