├── .npmrc ├── .travis.yml ├── .github └── FUNDING.yml ├── license ├── .gitignore ├── package.json ├── readme.md ├── fastic.js └── gif.svg /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: xxczaki 4 | patreon: akepinski 5 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Antoni Kepinski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fastic", 3 | "description": "Fast & Lightweight CLI HTTP server", 4 | "version": "1.6.1", 5 | "bin": { 6 | "fastic": "./fastic.js" 7 | }, 8 | "preferGlobal": true, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/xxczaki/fastic.git" 12 | }, 13 | "author": { 14 | "name": "Antoni Kepinski", 15 | "email": "a@kepinski.me", 16 | "url": "https://kepinski.me" 17 | }, 18 | "license": "MIT", 19 | "keywords": [ 20 | "fastic", 21 | "server", 22 | "cli", 23 | "http", 24 | "fast", 25 | "nodejs", 26 | "static", 27 | "web", 28 | "site", 29 | "website" 30 | ], 31 | "devDependencies": { 32 | "xo": "*" 33 | }, 34 | "dependencies": { 35 | "boxen": "^4.1.0", 36 | "chalk": "^2.4.2", 37 | "clipboardy": "^2.1.0", 38 | "directory-exists": "^2.0.1", 39 | "meow": "^5.0.0", 40 | "open": "^6.4.0", 41 | "turbo-http": "^0.3.2", 42 | "v8-compile-cache": "^2.1.0" 43 | }, 44 | "scripts": { 45 | "test": "xo" 46 | }, 47 | "xo": { 48 | "rules": { 49 | "handle-callback-err": 0, 50 | "import/no-unassigned-import": 0 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Fastic 🚀 2 | 3 | > Fast & Lightweight HTTP server, that just works. Accessible through CLI. 4 | 5 | [![Build Status](https://travis-ci.org/xxczaki/fastic.svg?branch=master)](https://travis-ci.org/xxczaki/fastic) [![XO code style](https://img.shields.io/badge/code_style-XO-5ed9c7.svg)](https://github.com/xojs/xo) 6 | 7 | SVG 8 | 9 | # Highlights 10 | - Beautiful output 11 | - Zero-config (unless you want to specify a custom port or directory). 12 | - Uses async/await 13 | - Easy access through CLI. 14 | - Automatically detects the content type, using file extension. 15 | - Uses blazing fast [turbo-http](https://github.com/mafintosh/turbo-http) library. 16 | - Logs HTTP requests & response status codes. 17 | - Single source file (containing ~200 lines of code) 18 | 19 | # Install 20 | ```bash 21 | npm install --global fastic 22 | ``` 23 | You can also use `npx`: 24 | 25 | ```bash 26 | npx fastic 27 | ``` 28 | 29 | # Usage 30 | 31 | ```bash 32 | Usage 33 | $ fastic 34 | Options 35 | --port, -p Port on which the server will be running (default: 5050) 36 | --directory, -d Directory from which the server will be running (default: current path) 37 | --open, -o Open server address in browser? (default: false) 38 | --log, -l Log HTTP requests & response status codes (default: false) 39 | Examples 40 | $ fastic 41 | $ fastic -p 8080 -d dist --open 42 | $ fastic --port 3000 --log 43 | ``` 44 | 45 | ## License 46 | 47 | MIT 48 | 49 | -------------------------------------------------------------------------------- /fastic.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | require('v8-compile-cache'); 6 | 7 | const fs = require('fs'); 8 | const path = require('path'); 9 | const turbo = require('turbo-http'); 10 | const meow = require('meow'); 11 | const chalk = require('chalk'); 12 | const boxen = require('boxen'); 13 | const open = require('open'); 14 | const clipboardy = require('clipboardy'); 15 | const directoryExists = require('directory-exists'); 16 | 17 | // CLI Configuration 18 | const cli = meow(` 19 | Usage 20 | $ fastic 21 | Options 22 | --port, -p Port on which the server will be running (default: 5050) 23 | --directory, -d Directory from which the server will be running (default: current path) 24 | --open, -o Open server address in browser? (default: false) 25 | --log, -l Log HTTP requests & response status codes (default: false) 26 | Examples 27 | $ fastic 28 | $ fastic -p 8080 -d dist --open 29 | $ fastic --port 3000 --log 30 | `, { 31 | flags: { 32 | port: { 33 | type: 'string', 34 | alias: 'p', 35 | default: '5050' 36 | }, 37 | directory: { 38 | type: 'string', 39 | alias: 'd', 40 | default: '.' 41 | }, 42 | open: { 43 | type: 'boolean', 44 | alias: 'o', 45 | default: false 46 | }, 47 | log: { 48 | type: 'boolean', 49 | alias: 'l', 50 | default: false 51 | } 52 | } 53 | }); 54 | 55 | const {port, directory} = cli.flags; 56 | 57 | // Port validation 58 | if (port < 1024 || port > 65535) { 59 | console.log(chalk.red('Invalid port number! It should fit in range between 1024 and 65535.')); 60 | process.exit(1); 61 | } else if (isNaN(port)) { 62 | console.log(chalk.red(port, 'is not a port number!')); 63 | process.exit(1); 64 | } 65 | 66 | // Directory validation 67 | if (directoryExists(directory) === false) { 68 | console.log(chalk.red(directory, 'is not a directory.')); 69 | process.exit(1); 70 | } 71 | 72 | // Detect content type using file extension 73 | const getTypes = () => { 74 | return { 75 | '.avi': 'video/avi', 76 | '.bmp': 'image/bmp', 77 | '.css': 'text/css', 78 | '.gif': 'image/gif', 79 | '.svg': 'image/svg+xml', 80 | '.htm': 'text/html', 81 | '.html': 'text/html', 82 | '.ico': 'image/x-icon', 83 | '.jpeg': 'image/jpeg', 84 | '.jpg': 'image/jpeg', 85 | '.js': 'text/javascript', 86 | '.json': 'application/json', 87 | '.mov': 'video/quicktime', 88 | '.mp3': 'audio/mpeg3', 89 | '.mpa': 'audio/mpeg', 90 | '.mpeg': 'video/mpeg', 91 | '.mpg': 'video/mpeg', 92 | '.oga': 'audio/ogg', 93 | '.ogg': 'application/ogg', 94 | '.ogv': 'video/ogg', 95 | '.pdf': 'application/pdf', 96 | '.png': 'image/png', 97 | '.tif': 'image/tiff', 98 | '.tiff': 'image/tiff', 99 | '.txt': 'text/plain', 100 | '.wav': 'audio/wav', 101 | '.xml': 'text/xml' 102 | }; 103 | }; 104 | 105 | const types = getTypes(); 106 | 107 | // Set headers 108 | const sendFile = async (res, type, content) => { 109 | await res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); 110 | await res.setHeader('Clear-Site-Data', 'cache', 'cookies'); 111 | await res.setHeader('Pragma', 'no-cache'); 112 | await res.setHeader('Expires', '0'); 113 | await res.setHeader('Content-Type', type); 114 | res.end(content); 115 | }; 116 | 117 | // Interface for listing the directory's contents 118 | const sendDirListing = (res, files, dirs, requestPath) => { 119 | requestPath = ('/' + requestPath).replace(/\/+/g, '/'); 120 | const content = ` 121 | 122 |

Index of ${requestPath}

123 | 135 | 136 | 137 | `; 138 | res.end(content); 139 | }; 140 | 141 | // Directory listing 142 | const listDirectory = async (res, dir, requestPath) => { 143 | await res.setHeader('Content-Type', 'text/html'); 144 | await res.setHeader('Clear-Site-Data', 'cache', 'cookies'); 145 | await res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); 146 | await res.setHeader('Pragma', 'no-cache'); 147 | await res.setHeader('Expires', '0'); 148 | 149 | fs.readdir(dir, (err, fileNames) => { 150 | let numRemaining = fileNames.length; 151 | const files = []; 152 | const dirs = []; 153 | 154 | fileNames.forEach(name => { 155 | fs.stat(path.join(dir, name), (err, stat) => { 156 | if (stat) { 157 | if (stat.isDirectory()) { 158 | dirs.push(`${name}/`); 159 | } else { 160 | files.push(name); 161 | } 162 | } 163 | 164 | if (!--numRemaining) { 165 | sendDirListing(res, files, dirs, requestPath); 166 | } 167 | }); 168 | }); 169 | }); 170 | }; 171 | 172 | // Server 173 | turbo.createServer(async (req, res) => { 174 | const {method, url} = req; 175 | let requestPath = decodeURI(url.replace(/^\/+/, '').replace(/\?.*$/, '')); 176 | const filePath = path.resolve(directory, requestPath); 177 | const type = types[path.extname(filePath)] || 'application/octet-stream'; 178 | // Logger 179 | fs.stat(filePath, (err, stat) => { 180 | if (stat && stat.isDirectory()) { 181 | fs.readFile(filePath + '/index.html', (err, content) => { 182 | if (err) { 183 | requestPath = (requestPath + '/').replace(/\/+$/, '/'); 184 | listDirectory(res, filePath, requestPath); 185 | if (cli.flags.log) { 186 | console.log(`${chalk.green('Fastic')} ${chalk.dim('›')}`, `${chalk.cyan(method)}`, `${chalk.yellow.bold(200)}`, url); 187 | } 188 | } else { 189 | sendFile(res, 'text/html', content); 190 | if (cli.flags.log) { 191 | console.log(`${chalk.green('Fastic')} ${chalk.dim('›')}`, `${chalk.cyan(method)}`, `${chalk.yellow.bold(200)}`, url); 192 | } 193 | } 194 | }); 195 | } else { 196 | fs.readFile(filePath, (err, content) => { 197 | if (err) { 198 | if (cli.flags.log) { 199 | console.log(`${chalk.green('Fastic')} ${chalk.dim('›')}`, `${chalk.cyan(method)}`, `${chalk.red.bold(404)}`, url); 200 | } 201 | } else { 202 | sendFile(res, type, content); 203 | if (cli.flags.log) { 204 | console.log(`${chalk.green('Fastic')} ${chalk.dim('›')}`, `${chalk.cyan(method)}`, `${chalk.yellow.bold(200)}`, url); 205 | } 206 | } 207 | }); 208 | } 209 | }); 210 | }).listen(port, () => { 211 | // Notify user about server & open it in browser 212 | console.log(boxen( 213 | `${chalk.green('Fastic')} ${chalk.dim('›')} Running at ${chalk.cyan('127.0.0.1:' + port)} ${cli.flags.open ? chalk.dim('[opened in browser]') : chalk.dim('[copied to clipboard]')}\n\n=> Press Ctrl + C to stop` 214 | , {padding: 1, borderStyle: 'round'})); 215 | 216 | if (cli.flags.open) { 217 | open(`http://127.0.0.1:${port}`); 218 | } else { 219 | clipboardy.write(`http://127.0.0.1:${port}`); 220 | } 221 | }); 222 | 223 | // Show message, when Ctrl + C is pressed 224 | process.on('SIGINT', () => { 225 | console.log(`\n${chalk.green('Fastic')} ${chalk.dim('›')} Stopped, see you next time!`); 226 | process.exit(0); 227 | }); 228 | -------------------------------------------------------------------------------- /gif.svg: -------------------------------------------------------------------------------- 1 | ~fasticfastic8210fastic8210~/DEV/kepinskifasticRunningathttp://localhost:8210[copiedtoclipboard]=>PressCtrl+CtostopfasticGET200/fasticGET200/css/landing.cssfasticGET200/js/click.jsfasticGET200/fonts/icon_email.svgfasticGET200/fonts/icon_github.svgfasticGET200/fonts/icon_twitter.svgfasticGET200/images/avatar.jpgfasticGET200/images/favicon-32x32.pngfasticGET200/images/favicon-16x16.pngfasticGET404/foobar^C~took15sffafasfastfastifastic8fastic82fastic821fastic8210~fastic8210~/fastic8210~/Dfastic8210~/DEfastic8210~/DEVfastic8210~/DEV/fastic8210~/DEV/kfastic8210~/DEV/kefastic8210~/DEV/kepfastic8210~/DEV/kepifastic8210~/DEV/kepinfastic8210~/DEV/kepinsfastic8210~/DEV/kepinsk --------------------------------------------------------------------------------