├── .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 | [](https://travis-ci.org/xxczaki/fastic) [](https://github.com/xojs/xo)
6 |
7 |
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 |
124 | ${
125 | dirs.map(dir => {
126 | return '- 📁 ' + dir + '
';
127 | }).join('')
128 | }
129 | ${
130 | files.map(file => {
131 | return '- 📄 ' + file + '
';
132 | }).join('')
133 | }
134 |
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 |
--------------------------------------------------------------------------------