├── .editorconfig ├── .github └── FUNDING.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .prettierignore ├── .prettierrc ├── .travis.yml ├── .vscode └── launch.json ├── .yarnrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── assets ├── images │ └── here.jpg └── psd │ └── here.psd ├── constant ├── config.js └── log-prefix.js ├── debug.js ├── index.js ├── lib ├── application.js ├── build-file-list.js ├── config.js ├── fallback-content-type.js ├── get-file-stat.js ├── read-file.js ├── read-folder.js ├── server.js └── watcher.js ├── middleware ├── error.js ├── file-explorer.js ├── gzip.js ├── live-reload.js ├── load-route.js └── log.js ├── package-lock.json ├── package.json ├── renovate.json ├── res ├── list.pug └── reload.js └── test ├── command.js ├── file ├── css │ └── index.css ├── folder │ ├── ddd.html │ ├── icon-search.png │ └── 搜索框.png ├── here.js ├── index.html ├── js │ └── index.js └── mock-server │ ├── ajax │ └── test.json └── test └── commander.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | curly_bracket_next_line = false 11 | spaces_around_operators = true 12 | indent_brace_style = 1tbs 13 | 14 | [*.js] 15 | quote_type = single 16 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: vivaxy 2 | open_collective: vivaxy_personal 3 | custom: ['https://gist.github.com/vivaxy/58eed1803a2eddda05c90aed99430de2'] 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | npm-debug.log 3 | node_modules 4 | .DS_Store 5 | yarn.lock 6 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | CHANGELOG.md 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5", 4 | "arrowParens": "always" 5 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | - "lts/*" 5 | os: osx 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "skipFiles": [ 12 | "/**" 13 | ], 14 | "program": "${workspaceFolder}/index.js" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | registry "https://registry.npmjs.org/" 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [3.5.0](https://github.com/vivaxy/here/compare/v3.4.1...v3.5.0) (2024-05-23) 6 | 7 | 8 | ### Features 9 | 10 | * **ip:** add ip option ([703d306](https://github.com/vivaxy/here/commit/703d306ef2ea4b36170f6b07e7752d320eb9f1f7)) 11 | 12 | ### [3.4.1](https://github.com/vivaxy/here/compare/v3.4.0...v3.4.1) (2021-02-07) 13 | 14 | 15 | ### Bug Fixes 16 | 17 | * **deps:** update dependency commander to v7 ([342a2bb](https://github.com/vivaxy/here/commit/342a2bb9a3552a692a78479c6c61cfc04acb0ff2)) 18 | * **deps:** update dependency koa-compress to v4 ([a74ddd4](https://github.com/vivaxy/here/commit/a74ddd4b2bad0aa74a4ecf663289cdf3d43633c1)) 19 | * **deps:** update dependency koa-router to v8 ([27ceb5d](https://github.com/vivaxy/here/commit/27ceb5db4103afe28863a5a58e2ac85df8c2b1a3)) 20 | * **deps:** update dependency mime to v2 ([af7298d](https://github.com/vivaxy/here/commit/af7298dcf13bbe2192c56d62ae840d12657af4d4)) 21 | * :bug: fix file mime and commander options ([e419d5f](https://github.com/vivaxy/here/commit/e419d5f1662c55eae0cadc182d890ac0c22c24a6)) 22 | 23 | 24 | # [3.4.0](https://github.com/vivaxy/here/compare/v3.3.0...v3.4.0) (2019-07-10) 25 | 26 | 27 | ### Features 28 | 29 | * **file explorer:** :sparkles: add Access-Control-Allow-Origin * for all files ([787cae9](https://github.com/vivaxy/here/commit/787cae9)) 30 | 31 | 32 | 33 | 34 | # [3.3.0](https://github.com/vivaxy/here/compare/v3.2.2...v3.3.0) (2018-10-12) 35 | 36 | 37 | ### Features 38 | 39 | * **gzip:** :sparkles:Support gzip compression ([1fb8732](https://github.com/vivaxy/here/commit/1fb8732)) 40 | * Drop support for node.js v6 and v7 ([74c277d](https://github.com/vivaxy/here/commit/74c277d)) 41 | 42 | 43 | 44 | 45 | ## [3.2.2](https://github.com/vivaxy/here/compare/v3.2.1...v3.2.2) (2017-12-28) 46 | 47 | 48 | ### Bug Fixes 49 | 50 | * :lock:Fix an issue that users can browse outside the wd ([d50c567](https://github.com/vivaxy/here/commit/d50c567)), closes [#16](https://github.com/vivaxy/here/issues/16) 51 | 52 | 53 | 54 | 55 | ## [3.2.1](https://github.com/vivaxy/here/compare/v3.1.0...v3.2.1) (2017-04-11) 56 | 57 | 58 | 59 | # v3.2.0 60 | 61 | - support restful API, config same as `koa-router` 62 | - upgrade jade to pug 63 | - update node versions in travis ci 64 | - add verbose log for request method, path and cost time 65 | - fix directory config 66 | - change server response log from debug level to verbose 67 | - add ssl test case 68 | - update readme 69 | 70 | # v3.1.0 71 | 72 | - add ssl support 73 | 74 | # v3.0.1 75 | 76 | - remove usage report 77 | 78 | # v3.0.0 79 | 80 | - rewrite in es6 81 | 82 | - use co 83 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Publish 2 | 3 | `npm run release` 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 vivaxy 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # here 2 | 3 | ![here](./assets/images/here.jpg) 4 | 5 | [![Build Status][travis-image]][travis-url] 6 | [![NPM Version][npm-version-image]][npm-url] 7 | [![NPM Downloads][npm-downloads-image]][npm-url] 8 | [![MIT License][license-image]][license-url] 9 | 10 | Local static server 11 | 12 | Everything start from `here`. 13 | 14 | Node.js >= 8.0 15 | 16 | ## Feature 17 | 18 | - Look up available port automatically, which means multiple instances without specifying port. 19 | 20 | - Custom routes by scripting `here.js`. 21 | 22 | - Live reload. 23 | 24 | - Support https. 25 | 26 | - Add ip address to your server, which makes your server available to other devices. 27 | 28 | - Resolve get, post... every method into local files, for ajax. 29 | 30 | - Respond files without extension as `application/json` for ajax. 31 | 32 | - Open default browser after server launched. 33 | 34 | - When the server is on, press `enter` will open the browser. 35 | 36 | ## Installation 37 | 38 | `[sudo] npm install -g @vivaxy/here` 39 | 40 | ## Usage 41 | 42 | In your local folder, type `here` and it goes\! 43 | 44 | ## Advanced Usage 45 | 46 | #### Specify port to 8888 47 | 48 | `here -p 8888` 49 | 50 | or 51 | 52 | `here --port 8888` 53 | 54 | Default port is `3000`. 55 | 56 | #### Switch protocol to https 57 | 58 | `here -S` 59 | 60 | or 61 | 62 | `here --ssl` 63 | 64 | #### Open Gzip 65 | 66 | `here -G` 67 | 68 | or 69 | 70 | `here --gzip` 71 | 72 | #### Specify server root directory 73 | 74 | `here -d test` 75 | 76 | or 77 | 78 | `here --directory test` 79 | 80 | Default directory is `./`. 81 | 82 | #### Watch file changes, once files changed, reload pages 83 | 84 | `here -w 3` 85 | 86 | or 87 | 88 | `here --watch` 89 | 90 | Default interval is 0 second. 91 | 92 | Recommend to set reload interval to page reload time. 93 | 94 | #### Do not open the browser 95 | 96 | `here -s` 97 | 98 | or 99 | 100 | `here --silent` 101 | 102 | #### Specify open browser IP 103 | 104 | `here --ip localhost` 105 | 106 | or 107 | 108 | `here --ip private` 109 | 110 | or 111 | 112 | `here --ip public` 113 | 114 | Default IP is `public`. 115 | 116 | #### Output log 117 | 118 | `here -l` 119 | 120 | or 121 | 122 | `here --log 0` 123 | 124 | #### Middleware support 125 | 126 | Write `here.js` in server base directory. 127 | 128 | ``` 129 | let db = { 130 | tobi: { 131 | name: 'tobi', 132 | age: 21 133 | }, 134 | loki: { 135 | name: 'loki', 136 | age: 26 137 | }, 138 | jane: { 139 | name: 'jane', 140 | age: 18 141 | } 142 | }; 143 | 144 | module.exports = [ 145 | { 146 | method: 'get', 147 | path: '/pets', 148 | data () { 149 | let names = Object.keys(db); 150 | return names.map((name) => { 151 | return db[name]; 152 | }); 153 | } 154 | }, 155 | { 156 | method: 'get', 157 | path: '/pets/:name', 158 | data () { 159 | let name = this.params.name; 160 | let pet = db[name]; 161 | if (!pet) { 162 | return { 163 | error: `cannot find pet ${name}` 164 | }; 165 | } else { 166 | return pet; 167 | } 168 | } 169 | } 170 | ]; 171 | ``` 172 | 173 | See [koa-router document](https://github.com/alexmingoia/koa-router#module_koa-router--Router+get%7Cput%7Cpost%7Cpatch%7Cdelete) for more detail. 174 | 175 | ## Prior Art 176 | 177 | - [puer](https://www.npmjs.com/package/puer) not support post, respond files without extension as `application/octet-stream` 178 | - [anywhere](https://www.npmjs.com/package/anywhere) not support post, and not support reload 179 | - [browsersync](http://www.browsersync.io/) not support post, respond files without extension as `application/octet-stream` 180 | - [serve](https://github.com/zeit/serve) 181 | 182 | ## Change Log 183 | 184 | [Change Log](CHANGELOG.md) 185 | 186 | ## Contributing 187 | 188 | [Contributing](CONTRIBUTING.md) 189 | 190 | [npm-version-image]: http://img.shields.io/npm/v/@vivaxy/here.svg?style=flat-square 191 | [npm-url]: https://www.npmjs.com/package/@vivaxy/here 192 | [npm-downloads-image]: http://img.shields.io/npm/dt/@vivaxy/here.svg?style=flat-square 193 | [license-image]: http://img.shields.io/npm/l/@vivaxy/here.svg?style=flat-square 194 | [license-url]: LICENSE 195 | [travis-image]: https://img.shields.io/travis/vivaxy/here.svg?style=flat-square 196 | [travis-url]: https://travis-ci.org/vivaxy/here 197 | -------------------------------------------------------------------------------- /assets/images/here.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vivaxy/here/81a5cf8261c03c1390a25d75960c2382cccc3d27/assets/images/here.jpg -------------------------------------------------------------------------------- /assets/psd/here.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vivaxy/here/81a5cf8261c03c1390a25d75960c2382cccc3d27/assets/psd/here.psd -------------------------------------------------------------------------------- /constant/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 2016-08-21 10:25 3 | * @author vivaxy 4 | */ 5 | 6 | exports.IS_DEBUG = 'isDebug'; 7 | exports.LOG_LEVEL = 'logLevel'; 8 | exports.DIRECTORY = 'directory'; 9 | exports.SERVE_HERE_VERSION = 'serveHereVersion'; 10 | exports.PORT = 'port'; 11 | exports.IP = 'ip'; 12 | exports.SSL = 'ssl'; 13 | exports.WATCH = 'watch'; 14 | exports.SILENT = 'silent'; 15 | exports.GZIP = 'gzip'; 16 | -------------------------------------------------------------------------------- /constant/log-prefix.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 2016-08-21 10:23 3 | * @author vivaxy 4 | */ 5 | 6 | exports.BROWSER = ' browser:'; 7 | exports.SERVER = ' server:'; 8 | exports.WATCH = ' watch:'; 9 | exports.RESPONSE = 'response:'; 10 | exports.REQUEST = ' request:'; 11 | exports.TIME = ' time:'; 12 | -------------------------------------------------------------------------------- /debug.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * @since 2015-11-06 13:47 5 | * @author vivaxy 6 | */ 7 | 8 | const config = require('./lib/config.js'); 9 | const configKeys = require('./constant/config.js'); 10 | 11 | config.set(configKeys.IS_DEBUG, true); 12 | 13 | require('./lib/application')(); 14 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * @since 2015-11-06 13:35 5 | * @author vivaxy 6 | */ 7 | 8 | require('./lib/application')(); 9 | -------------------------------------------------------------------------------- /lib/application.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 2015-11-20 13:41 3 | * @author vivaxy 4 | */ 5 | 6 | const path = require('path'); 7 | 8 | const log = require('log-util'); 9 | const commander = require('commander'); 10 | 11 | const config = require('./config.js'); 12 | const startServer = require('./server.js'); 13 | const startWatcher = require('./watcher.js'); 14 | const packageJson = require('../package.json'); 15 | const configKeys = require('../constant/config.js'); 16 | 17 | const formatWatch = (value) => { 18 | if (isNaN(Number(value))) { 19 | return 0; 20 | } else if (value < 0) { 21 | return 0; 22 | } 23 | return value; 24 | }; 25 | 26 | const formatDirectory = (value) => { 27 | const absoluteWorkingDirectory = process.cwd(); 28 | if (path.isAbsolute(value)) { 29 | return value; 30 | } 31 | return path.join(absoluteWorkingDirectory, value); 32 | }; 33 | 34 | const prepare = () => { 35 | if (config.get(configKeys.IS_DEBUG)) { 36 | config.set(configKeys.LOG_LEVEL, 0); 37 | } 38 | 39 | config.set(configKeys.DIRECTORY, formatDirectory('.')); 40 | config.set(configKeys.SERVE_HERE_VERSION, packageJson.version); 41 | 42 | commander 43 | .version(config.get(configKeys.SERVE_HERE_VERSION), '-v, --version') 44 | .option( 45 | `-p, --${configKeys.PORT} [port]`, 46 | 'specify port', 47 | config.get(configKeys.PORT) 48 | ) 49 | .option(`-S, --${configKeys.SSL}`, 'switch protocol to https') 50 | .option(`-G, --${configKeys.GZIP}`, 'open gzip') 51 | .option( 52 | `-d, --${configKeys.DIRECTORY} [directory]`, 53 | 'specify root directory', 54 | formatDirectory, 55 | config.get(configKeys.DIRECTORY) 56 | ) 57 | .option( 58 | `-w, --${configKeys.WATCH} [interval]`, 59 | 'will watch files; once changed, reload pages', 60 | formatWatch 61 | ) 62 | .option(`-s, --${configKeys.SILENT}`, 'will not open browser') 63 | .option(`--ip [ip]`, 'open browser with ip', config.get(configKeys.IP)) 64 | .option( 65 | `-l, --${configKeys.LOG_LEVEL} [level]`, 66 | 'output log in some level, lower means more details', 67 | config.get(configKeys.LOG_LEVEL) 68 | ) 69 | .parse(process.argv); 70 | 71 | const commanderOptions = commander.opts(); 72 | 73 | log.setLevel(Number(commanderOptions[configKeys.LOG_LEVEL])); 74 | 75 | if (commanderOptions[configKeys.PORT]) { 76 | config.set(configKeys.PORT, commanderOptions[configKeys.PORT]); 77 | } 78 | if (commanderOptions[configKeys.DIRECTORY]) { 79 | config.set(configKeys.DIRECTORY, commanderOptions[configKeys.DIRECTORY]); 80 | } 81 | 82 | if (commanderOptions[configKeys.SSL]) { 83 | config.set(configKeys.SSL, commanderOptions[configKeys.SSL]); 84 | } 85 | if (commanderOptions[configKeys.GZIP]) { 86 | config.set(configKeys.GZIP, commanderOptions[configKeys.GZIP]); 87 | } 88 | 89 | if (commanderOptions[configKeys.WATCH] !== undefined) { 90 | if (commanderOptions[configKeys.WATCH] === true) { 91 | config.set(configKeys.WATCH, 0); 92 | } else { 93 | config.set(configKeys.WATCH, commanderOptions[configKeys.WATCH]); 94 | } 95 | } 96 | if (commanderOptions[configKeys.SILENT]) { 97 | config.set(configKeys.SILENT, commanderOptions[configKeys.SILENT]); 98 | } 99 | 100 | if (commanderOptions[configKeys.IP]) { 101 | config.set(configKeys.IP, commanderOptions[configKeys.IP]); 102 | } 103 | 104 | if (commanderOptions[configKeys.LOG_LEVEL]) { 105 | config.set(configKeys.LOG_LEVEL, commanderOptions[configKeys.LOG_LEVEL]); 106 | } 107 | }; 108 | 109 | module.exports = () => { 110 | prepare(); 111 | 112 | if (!config.get(configKeys.IS_DEBUG)) { 113 | process.on('uncaughtException', (e) => { 114 | // throw this to preserve default behaviour 115 | // console this instead of throw error to keep the original error trace 116 | log.error(e.stack); 117 | // still exit as uncaught exception 118 | }); 119 | } 120 | 121 | startServer((nativeServer) => { 122 | if (config.get(configKeys.WATCH) !== false) { 123 | startWatcher(nativeServer); 124 | } 125 | }); 126 | }; 127 | -------------------------------------------------------------------------------- /lib/build-file-list.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 150205 09:21 3 | * @author vivaxy 4 | */ 5 | 6 | const fs = require('fs'); 7 | const path = require('path'); 8 | 9 | const pug = require('pug'); 10 | 11 | const join = path.join; 12 | const extname = path.extname; 13 | const isWindows = /^win/.test(process.platform); 14 | 15 | const AHEAD = -1; 16 | const ABACK = 1; 17 | const STILL = 0; 18 | const HTML_EXTENSION = '.html'; 19 | 20 | /** 21 | * 22 | * @param {String} _a fileName 23 | * @param {String} _b fileName 24 | * @returns {number} sort value 25 | */ 26 | const compare = (_a, _b) => { 27 | if (_a < _b) { 28 | return AHEAD; 29 | } else if (_a > _b) { 30 | return ABACK; 31 | } 32 | return STILL; 33 | }; 34 | 35 | /** 36 | * 37 | * @param {Array} files folder file list 38 | * @param {String} pathname folder absolute path 39 | * @param {String} absoluteWorkingDirectory absolute root directory 40 | * @returns {String} html content 41 | */ 42 | module.exports = (files, pathname, absoluteWorkingDirectory) => { 43 | const baseDir = join(absoluteWorkingDirectory, pathname); 44 | 45 | const filesFiltered = files.filter((file) => { 46 | if (file.indexOf('.') === 0) { 47 | return false; 48 | } 49 | // #3 remove inaccessible files from file list 50 | // win下access任何文件都会返回可访问 51 | try { 52 | if (isWindows) { 53 | fs.statSync(join(baseDir, file)); 54 | } else { 55 | fs.accessSync(join(baseDir, file), fs.R_OK); 56 | } 57 | } catch (e) { 58 | return false; 59 | } 60 | return true; 61 | }); 62 | 63 | filesFiltered.sort((a, b) => { 64 | const _isDirectoryA = fs.lstatSync(join(baseDir, a)).isDirectory(); 65 | const _isDirectoryB = fs.lstatSync(join(baseDir, b)).isDirectory(); 66 | // order by priority 67 | if (_isDirectoryA && _isDirectoryB) { 68 | return compare(a, b); 69 | } 70 | if (_isDirectoryA && !_isDirectoryB) { 71 | return AHEAD; 72 | } 73 | if (_isDirectoryB && !_isDirectoryA) { 74 | return ABACK; 75 | } 76 | const _extensionA = extname(a); 77 | const _extensionB = extname(b); 78 | const _isHtmlA = _extensionA === HTML_EXTENSION; 79 | const _isHtmlB = _extensionB === HTML_EXTENSION; 80 | if (_isHtmlA && _isHtmlB) { 81 | return compare(a, b); 82 | } 83 | if (_isHtmlA && !_isHtmlB) { 84 | return AHEAD; 85 | } 86 | if (_isHtmlB && !_isHtmlA) { 87 | return ABACK; 88 | } 89 | if (_extensionA === _extensionB) { 90 | return compare(a, b); 91 | } 92 | return compare(a, b); 93 | }); 94 | 95 | if (pathname !== '/') { 96 | filesFiltered.unshift('..'); 97 | } 98 | 99 | const list = filesFiltered.map((file) => { 100 | const ext = extname(file); 101 | let _ext = 'other'; 102 | if (ext === HTML_EXTENSION) { 103 | _ext = 'html'; 104 | } 105 | if (fs.lstatSync(join(baseDir, file)).isDirectory()) { 106 | _ext = 'dir'; 107 | } 108 | if (file === '..') { 109 | _ext = 'null'; 110 | } 111 | return { 112 | // remove http://ip:port to redirect to correct ip:port 113 | href: join(pathname, file), 114 | className: _ext, 115 | fileName: file, 116 | }; 117 | }); 118 | 119 | return pug.compileFile(join(__dirname, '../res/list.pug'), { 120 | pretty: ' ', 121 | })({ list }); 122 | }; 123 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 2015-11-20 15:40 3 | * @author vivaxy 4 | */ 5 | 6 | const configKeys = require('../constant/config.js'); 7 | 8 | const config = { 9 | [configKeys.IS_DEBUG]: false, 10 | 11 | [configKeys.PORT]: 3000, 12 | [configKeys.SSL]: false, 13 | [configKeys.GZIP]: false, 14 | [configKeys.DIRECTORY]: '.', 15 | [configKeys.WATCH]: false, 16 | [configKeys.SILENT]: false, 17 | [configKeys.IP]: 'public', 18 | [configKeys.LOG_LEVEL]: 2, 19 | 20 | [configKeys.SERVE_HERE_VERSION]: '0.0.0', 21 | }; 22 | 23 | exports.set = (key, value) => { 24 | config[key] = value; 25 | return value; 26 | }; 27 | 28 | exports.get = (key) => { 29 | return config[key]; 30 | }; 31 | -------------------------------------------------------------------------------- /lib/fallback-content-type.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 2015-11-20 13:24 3 | * @author vivaxy 4 | */ 5 | 6 | module.exports = 'application/json'; 7 | -------------------------------------------------------------------------------- /lib/get-file-stat.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 2015-11-23 10:14 3 | * @author vivaxy 4 | */ 5 | 6 | const fs = require('fs'); 7 | 8 | module.exports = (file) => { 9 | return new Promise((resolve, reject) => { 10 | return fs.lstat(file, (err, stat) => { 11 | if (err) { 12 | reject(err); 13 | } else { 14 | resolve(stat); 15 | } 16 | }); 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /lib/read-file.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 2015-11-20 20:09 3 | * @author vivaxy 4 | */ 5 | 6 | const fs = require('fs'); 7 | 8 | module.exports = (file) => { 9 | return new Promise((resolve, reject) => { 10 | return fs.readFile(file, (err, content) => { 11 | if (err) { 12 | reject(err); 13 | } else { 14 | resolve(content); 15 | } 16 | }); 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /lib/read-folder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 2015-11-23 10:13 3 | * @author vivaxy 4 | */ 5 | 6 | const fs = require('fs'); 7 | 8 | module.exports = (file) => { 9 | return new Promise((resolve, reject) => { 10 | return fs.readdir(file, (err, files) => { 11 | if (err) { 12 | reject(err); 13 | } else { 14 | resolve(files); 15 | } 16 | }); 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 2015-11-19 20:15 3 | * @author vivaxy 4 | */ 5 | const path = require('path'); 6 | const https = require('https'); 7 | 8 | const ip = require('ip'); 9 | const Koa = require('koa'); 10 | const pem = require('pem'); 11 | const log = require('log-util'); 12 | const open = require('open'); 13 | 14 | const config = require('./config.js'); 15 | const getFileStat = require('./get-file-stat.js'); 16 | const logPrefix = require('../constant/log-prefix.js'); 17 | const configKeys = require('../constant/config.js'); 18 | 19 | module.exports = (callback) => { 20 | let nativeServer; 21 | const directory = config.get(configKeys.DIRECTORY); 22 | const watch = config.get(configKeys.WATCH); 23 | 24 | const server = new Koa(); 25 | 26 | const serverErrorCallback = (err) => { 27 | if (err.code === 'EADDRINUSE') { 28 | const port = config.get(configKeys.PORT); 29 | log.warn(logPrefix.SERVER, 'port', port, 'in use'); 30 | config.set(configKeys.PORT, port + 1); 31 | listen(); 32 | } 33 | }; 34 | 35 | const openBrowserFunction = (url) => { 36 | open(url) 37 | .then(() => log.debug(logPrefix.BROWSER, url)) 38 | .catch((e) => log.error(logPrefix.BROWSER, e.message)); 39 | }; 40 | 41 | function getIP() { 42 | const ipType = config.get(configKeys.IP); 43 | if (ipType === 'localhost') { 44 | return 'localhost'; 45 | } 46 | if (ipType === 'private') { 47 | return '127.0.0.1'; 48 | } 49 | return ip.address(); 50 | } 51 | 52 | const serverSuccessCallback = () => { 53 | const port = config.get(configKeys.PORT); 54 | const protocol = config.get(configKeys.SSL) ? 'https:' : 'http:'; 55 | const url = `${protocol}//${getIP()}:${port}/`; 56 | 57 | log.success(logPrefix.SERVER, 'listen', url); 58 | 59 | if (!config.get(configKeys.SILENT)) { 60 | openBrowserFunction(url); 61 | } 62 | // hit `space` will open page in browser 63 | const stdIn = process.stdin; 64 | stdIn.setEncoding('utf8'); 65 | stdIn.on('data', openBrowserFunction.bind(null, url)); 66 | callback(nativeServer); 67 | }; 68 | 69 | const listen = () => { 70 | const port = config.get(configKeys.PORT); 71 | if (config.get(configKeys.SSL)) { 72 | pem.createCertificate({ days: 1, selfSigned: true }, (err, keys) => { 73 | if (err) { 74 | log.error(err); 75 | return; 76 | } 77 | nativeServer = https 78 | .createServer( 79 | { key: keys.serviceKey, cert: keys.certificate }, 80 | server.callback() 81 | ) 82 | .on('error', serverErrorCallback) 83 | .listen(port, serverSuccessCallback); 84 | }); 85 | } else { 86 | nativeServer = server 87 | .listen(port, serverSuccessCallback) 88 | .on('error', serverErrorCallback); 89 | } 90 | }; 91 | 92 | const middlewareList = []; 93 | // - log request 94 | // - load route 95 | // - exec route 96 | // - gzip 97 | // - live reload 98 | // - read file 99 | // - handle error 100 | 101 | middlewareList.push(require('../middleware/log.js')); 102 | middlewareList.push(require('../middleware/error.js')); 103 | 104 | const routeFile = path.join(directory, 'here.js'); 105 | getFileStat(routeFile) 106 | .then((stat) => { 107 | if (stat.isFile()) { 108 | middlewareList.push(require('../middleware/load-route.js')(server)); 109 | } 110 | start(); 111 | }) 112 | .catch(() => { 113 | // routeFile not exists 114 | start(); 115 | }); 116 | 117 | function start() { 118 | if (config.get(configKeys.GZIP)) { 119 | middlewareList.push(require('../middleware/gzip.js')); 120 | } 121 | if (watch !== false) { 122 | middlewareList.push(require('../middleware/live-reload.js')); 123 | } 124 | middlewareList.push(require('../middleware/file-explorer.js')); 125 | middlewareList.forEach((middleware) => { 126 | server.use(middleware); 127 | }); 128 | listen(); 129 | } 130 | }; 131 | -------------------------------------------------------------------------------- /lib/watcher.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 2015-11-20 21:48 3 | * @author vivaxy 4 | */ 5 | 6 | const path = require('path'); 7 | 8 | const log = require('log-util'); 9 | const chokidar = require('chokidar'); 10 | const debounce = require('debounce'); 11 | const socketServer = require('socket.io'); 12 | 13 | const config = require('./config.js'); 14 | const configKeys = require('../constant/config.js'); 15 | const logPrefix = require('../constant/log-prefix.js'); 16 | 17 | const SPLICE_COUNT = 1; 18 | const NOT_FOUNT_INDEX = -1; 19 | const A_THOUSAND = 1000; 20 | 21 | module.exports = (server) => { 22 | const absoluteWorkingDirectory = config.get(configKeys.DIRECTORY); 23 | const interval = config.get(configKeys.WATCH); 24 | 25 | const socketList = []; 26 | 27 | const io = socketServer(server); 28 | 29 | io.on('connection', (socket) => { 30 | socketList.push(socket); 31 | socket.on('disconnect', () => { 32 | const index = socketList.indexOf(socket); 33 | if (index > NOT_FOUNT_INDEX) { 34 | socketList.splice(index, SPLICE_COUNT); 35 | } 36 | }); 37 | }); 38 | 39 | const reload = debounce(() => { 40 | socketList.forEach((socket) => { 41 | log.debug(logPrefix.WATCH, 'reload'); 42 | socket.emit('reload'); 43 | }); 44 | }, interval * A_THOUSAND); 45 | 46 | const watcher = chokidar.watch(absoluteWorkingDirectory, { 47 | ignored: [ 48 | '**/node_modules', 49 | /[\/\\]\./, 50 | '.git', 51 | '.idea', 52 | '.DS_Store', 53 | ], 54 | }); 55 | 56 | watcher.on('all', (action, filePath) => { 57 | log.debug(logPrefix.WATCH, action, path.relative(absoluteWorkingDirectory, filePath)); 58 | switch (action) { 59 | case 'add': 60 | case 'addDir': 61 | break; 62 | default: 63 | reload(); 64 | break; 65 | } 66 | }); 67 | 68 | watcher.on('ready', () => { 69 | log.success(logPrefix.WATCH, 'ready,', 'reload in', interval, 'seconds'); 70 | }); 71 | }; 72 | -------------------------------------------------------------------------------- /middleware/error.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 2015-11-20 13:11 3 | * @author vivaxy 4 | */ 5 | 6 | const FALLBACK_CONTENT_TYPE = require('../lib/fallback-content-type.js'); 7 | 8 | module.exports = async function(ctx, next) { 9 | try { 10 | await next(); 11 | } catch (e) { 12 | ctx.status = 404; 13 | ctx.body = e.stack; 14 | ctx.type = FALLBACK_CONTENT_TYPE; 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /middleware/file-explorer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 2015-11-20 13:11 3 | * @author vivaxy 4 | */ 5 | const path = require('path'); 6 | 7 | const mime = require('mime'); 8 | const log = require('log-util'); 9 | 10 | const configKeys = require('../constant/config.js'); 11 | const logPrefix = require('../constant/log-prefix.js'); 12 | 13 | const config = require('../lib/config.js'); 14 | const readFile = require('../lib/read-file.js'); 15 | const readFolder = require('../lib/read-folder.js'); 16 | const getFileStat = require('../lib/get-file-stat.js'); 17 | const buildFileBrowser = require('../lib/build-file-list.js'); 18 | const FALLBACK_CONTENT_TYPE = require('../lib/fallback-content-type.js'); 19 | 20 | const NOT_FOUNT_INDEX = -1; 21 | const INDEX_PAGE = 'index.html'; 22 | 23 | module.exports = async function(ctx, next) { 24 | const directory = config.get(configKeys.DIRECTORY); 25 | 26 | // decode for chinese character 27 | const requestPath = decodeURIComponent(ctx.request.path); 28 | const fullRequestPath = path.join(directory, requestPath); 29 | // fix security issue 30 | if (!fullRequestPath.startsWith(directory)) { 31 | return await next(); 32 | } 33 | const stat = await getFileStat(fullRequestPath); 34 | 35 | if (stat.isDirectory()) { 36 | const files = await readFolder(fullRequestPath); 37 | 38 | if (files.indexOf(INDEX_PAGE) !== NOT_FOUNT_INDEX) { 39 | ctx.redirect(path.join(requestPath, INDEX_PAGE), '/'); 40 | } else { 41 | ctx.body = buildFileBrowser(files, requestPath, directory); 42 | ctx.type = mime.getType(INDEX_PAGE); 43 | } 44 | } else if (stat.isFile()) { 45 | ctx.body = await readFile(fullRequestPath); 46 | let type = mime.getType(fullRequestPath); 47 | 48 | if (path.extname(fullRequestPath) === '') { 49 | type = FALLBACK_CONTENT_TYPE; 50 | } 51 | 52 | ctx.type = type; 53 | ctx.set('Access-Control-Allow-Origin', '*'); 54 | log.debug(logPrefix.RESPONSE, ctx.request.method, requestPath, 'as', type); 55 | } 56 | 57 | await next(); 58 | }; 59 | -------------------------------------------------------------------------------- /middleware/gzip.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 20181012 20:12 3 | * @author vivaxy 4 | */ 5 | 6 | const compress = require('koa-compress'); 7 | 8 | module.exports = compress(); 9 | -------------------------------------------------------------------------------- /middleware/live-reload.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 2015-11-20 16:03 3 | * @author vivaxy 4 | */ 5 | 6 | const path = require('path'); 7 | 8 | const readFile = require('../lib/read-file.js'); 9 | 10 | const NOT_FOUND_INDEX = 1; 11 | 12 | module.exports = async function(ctx, next) { 13 | await next(); 14 | 15 | if (ctx.type === 'text/html') { 16 | let response = ''; 17 | const body = ctx.body.toString('utf-8'); 18 | const reloadJavascriptContent = await readFile(path.join(__dirname, '../res/reload.js')); 19 | 20 | const reloadJavascript = ``; 21 | 22 | if (body.indexOf('') !== NOT_FOUND_INDEX) { 23 | const section = body.split(''); 24 | 25 | section.splice(1, 0, `${reloadJavascript}`); 26 | response = section.join(''); 27 | } else { 28 | response = body + reloadJavascript; 29 | } 30 | ctx.body = response; 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /middleware/load-route.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 2016-03-02 18:00 3 | * @author vivaxy 4 | */ 5 | 6 | const path = require('path'); 7 | const koaRoute = require('koa-router')(); 8 | 9 | const configKeys = require('../constant/config.js'); 10 | 11 | const config = require('../lib/config.js'); 12 | 13 | const loadRoutes = () => { 14 | const directory = config.get(configKeys.DIRECTORY); 15 | const file = path.join(directory, 'here.js'); 16 | const routeList = require(file); 17 | routeList.forEach((route) => { 18 | koaRoute[route.method](route.path, require('./log.js'), async function(ctx) { 19 | ctx.body = route.data.apply(ctx, arguments); 20 | }); 21 | }); 22 | }; 23 | 24 | module.exports = (server) => { 25 | loadRoutes(); 26 | 27 | server.use(koaRoute.routes()); 28 | server.use(koaRoute.allowedMethods()); 29 | 30 | return async function(ctx, next) { 31 | await next(); 32 | }; 33 | }; 34 | -------------------------------------------------------------------------------- /middleware/log.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 2016-08-21 10:13 3 | * @author vivaxy 4 | */ 5 | const log = require('log-util'); 6 | 7 | const logPrefix = require('../constant/log-prefix.js'); 8 | 9 | module.exports = async function(ctx, next) { 10 | const beginTime = new Date().getTime(); 11 | 12 | const request = ctx.request; 13 | log.debug(logPrefix.REQUEST, `${request.method} ${request.path}`); 14 | 15 | await next(); 16 | 17 | const endTime = new Date().getTime(); 18 | 19 | log.debug(logPrefix.TIME, `${endTime - beginTime}ms`); 20 | }; 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vivaxy/here", 3 | "version": "3.5.0", 4 | "description": "local static server", 5 | "homepage": "https://github.com/vivaxy/here", 6 | "bin": { 7 | "here": "./index.js" 8 | }, 9 | "engines": { 10 | "node": ">=8.0" 11 | }, 12 | "scripts": { 13 | "test": "mocha ./test/command.js --timeout 10000", 14 | "release": "standard-version && git push --follow-tags && npm publish --access=public", 15 | "postinstall": "husky install", 16 | "prepublishOnly": "pinst --disable", 17 | "postpublish": "pinst --enable" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/vivaxy/here.git" 22 | }, 23 | "bugs": { 24 | "url": "https://github.com/vivaxy/here/issues" 25 | }, 26 | "keywords": [ 27 | "server", 28 | "static server", 29 | "local server", 30 | "vivaxy", 31 | "tool", 32 | "web", 33 | "reload" 34 | ], 35 | "author": "vivaxy", 36 | "contributors": [ 37 | { 38 | "name": "vivaxy", 39 | "email": "xyxuye2007@126.com" 40 | } 41 | ], 42 | "license": "MIT", 43 | "devDependencies": { 44 | "husky": "5", 45 | "lint-staged": "^10.0.0", 46 | "mocha": "^8.0.0", 47 | "pinst": "^2.1.4", 48 | "prettier": "^2.0.0", 49 | "standard-version": "^9.0.0" 50 | }, 51 | "dependencies": { 52 | "chokidar": "^3.3.1", 53 | "commander": "^7.0.0", 54 | "debounce": "^1.0.0", 55 | "ip": "^1.1.0", 56 | "koa": "^2.5.3", 57 | "koa-compress": "^5.0.0", 58 | "koa-router": "^10.0.0", 59 | "log-util": "^2.3.0", 60 | "mime": "^2.0.0", 61 | "open": "^7.4.0", 62 | "pem": "^1.8.0", 63 | "pug": "^3.0.0", 64 | "socket.io": "^3.0.0" 65 | }, 66 | "lint-staged": { 67 | "**/**.{js,json,md}": [ 68 | "prettier --write" 69 | ] 70 | }, 71 | "packageManager": "npm@8.19.4" 72 | } 73 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base", 4 | ":preserveSemverRanges" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /res/list.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | meta(name='viewport', content='width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0') 5 | style. 6 | body { 7 | background: #eee; 8 | width: 100%; 9 | margin: 0; 10 | } 11 | 12 | a { 13 | display: block; 14 | height: 48px; 15 | width: 100%; 16 | text-decoration: none; 17 | background-color: #fff; 18 | border-bottom: 1px solid #ddd; 19 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0.1); 20 | -webkit-user-select: none; 21 | overflow: hidden; 22 | position: relative; 23 | } 24 | 25 | a span { 26 | display: block; 27 | height: 32px; 28 | line-height: 32px; 29 | font-size: 16px; 30 | color: #2980b9; 31 | margin: 8px 0 8px 32px; 32 | overflow: hidden; 33 | text-overflow: ellipsis; 34 | white-space: nowrap; 35 | } 36 | 37 | .html:before { 38 | margin: 4px 8px 0 0; 39 | background-image: url(data:image/png;base64,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); 40 | content: " "; 41 | background-size: 100%; 42 | width: 24px; 43 | height: 24px; 44 | float: left; 45 | } 46 | 47 | .dir:before { 48 | margin: 4px 8px 0 0; 49 | background-image: url(data:image/png;base64,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); 50 | content: " "; 51 | background-size: 100%; 52 | width: 24px; 53 | height: 24px; 54 | float: left; 55 | } 56 | 57 | .null:before { 58 | margin: 4px 8px 0 0; 59 | content: " "; 60 | width: 24px; 61 | height: 24px; 62 | float: left; 63 | } 64 | 65 | .other:before { 66 | margin: 4px 8px 0 0; 67 | background-image: url(data:image/png;base64,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); 68 | content: " "; 69 | background-size: 100%; 70 | width: 24px; 71 | height: 24px; 72 | float: left; 73 | } 74 | body 75 | each val in list 76 | a(href=val.href) 77 | span(class=val.className) #{val.fileName} 78 | -------------------------------------------------------------------------------- /test/command.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 2015-09-23 16:38 3 | * @author vivaxy 4 | */ 5 | 6 | const assert = require('assert'); 7 | const childProcess = require('child_process'); 8 | 9 | const packageJson = require('../package.json'); 10 | const logPrefix = require('../constant/log-prefix.js'); 11 | 12 | const spawn = childProcess.spawn; 13 | 14 | const NODE_COMMAND = 'node'; 15 | const HERE_COMMAND = './index.js'; 16 | 17 | describe('test terminal command `here`', () => { 18 | let here; 19 | afterEach(() => { 20 | here.kill(); 21 | }); 22 | it(`\`here\` should output \`${logPrefix.SERVER} listen http://*.*.*.*:*/\``, (done) => { 23 | here = spawn(NODE_COMMAND, [HERE_COMMAND]); 24 | here.stdout.on('data', (data) => { 25 | data = data.toString(); 26 | let regExp = `${logPrefix.SERVER} listen http:\\/\\/\\d+\\.\\d+\\.\\d+\\.\\d+:\\d+\\/\\n`; 27 | assert.equal(true, new RegExp(regExp, 'g').test(data)); 28 | here.kill(); 29 | done(); 30 | }); 31 | }); 32 | it('`here -v` should output `version`', (done) => { 33 | here = spawn(NODE_COMMAND, [HERE_COMMAND, '-v']); 34 | here.stdout.on('data', (data) => { 35 | data = data.toString(); 36 | assert.equal(packageJson.version + '\n', data); 37 | done(); 38 | }); 39 | }); 40 | it('`here --version` should output `version`', (done) => { 41 | here = spawn(NODE_COMMAND, [HERE_COMMAND, '--version']); 42 | here.stdout.on('data', (data) => { 43 | data = data.toString(); 44 | assert.equal(packageJson.version + '\n', data); 45 | done(); 46 | }); 47 | }); 48 | it('`here -h` should output help', (done) => { 49 | here = spawn(NODE_COMMAND, [HERE_COMMAND, '-h']); 50 | here.stdout.on('data', (data) => { 51 | data = data.toString(); 52 | assert.equal(true, !!~data.indexOf('Usage: index [options]') && !!~data.indexOf('Options:')); 53 | done(); 54 | }); 55 | }); 56 | it(`\`here -w\` should output \`${logPrefix.SERVER} listen http://*.*.*.*:*/\` and \`${logPrefix.WATCH} ready, reload in 0 seconds\``, (done) => { 57 | here = spawn(NODE_COMMAND, [HERE_COMMAND, '-w']); 58 | let stdoutCount = 0; 59 | here.stdout.on('data', (data) => { 60 | stdoutCount++; 61 | data = data.toString(); 62 | switch (stdoutCount) { 63 | case 1: 64 | let serverRegExp = `${logPrefix.SERVER} listen http:\\/\\/\\d+\\.\\d+\\.\\d+\\.\\d+:\\d+\\/\\n$`; 65 | assert.equal(true, new RegExp(serverRegExp).test(data)); 66 | break; 67 | case 2: 68 | let watchRegExp = `${logPrefix.WATCH} ready, reload in 0 seconds\\n$`; 69 | assert.equal(true, new RegExp(watchRegExp).test(data)); 70 | here.kill(); 71 | done(); 72 | break; 73 | default: 74 | assert.fail(stdoutCount, 2); 75 | done(); 76 | break; 77 | } 78 | }); 79 | }); 80 | it(`\`here --watch 3\` should output \`${logPrefix.SERVER} listen http://*.*.*.*:*/\` and \`${logPrefix.WATCH} ready, reload in 3 seconds\``, (done) => { 81 | here = spawn(NODE_COMMAND, [HERE_COMMAND, '--watch', '3']); 82 | let stdOutCount = 0; 83 | here.stdout.on('data', (data) => { 84 | stdOutCount++; 85 | data = data.toString(); 86 | switch (stdOutCount) { 87 | case 1: 88 | let serverRegExp = `${logPrefix.SERVER} listen http:\\/\\/\\d+\\.\\d+\\.\\d+\\.\\d+:\\d+\\/\\n$`; 89 | assert.equal(true, new RegExp(serverRegExp).test(data)); 90 | break; 91 | case 2: 92 | let watchRegExp = `${logPrefix.WATCH} ready, reload in 3 seconds\\n$`; 93 | assert.equal(true, new RegExp(watchRegExp).test(data)); 94 | here.kill(); 95 | done(); 96 | break; 97 | default: 98 | assert.fail(stdOutCount, 2); 99 | done(); 100 | break; 101 | } 102 | }); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /test/file/css/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | width: 100%; 4 | height: 100%; 5 | background: #eee; 6 | } 7 | 8 | .css:after { 9 | content: 'css ok'; 10 | } 11 | -------------------------------------------------------------------------------- /test/file/folder/ddd.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | vivaxy 8 | 9 | 10 | test 11 | 12 | -------------------------------------------------------------------------------- /test/file/folder/icon-search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vivaxy/here/81a5cf8261c03c1390a25d75960c2382cccc3d27/test/file/folder/icon-search.png -------------------------------------------------------------------------------- /test/file/folder/搜索框.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vivaxy/here/81a5cf8261c03c1390a25d75960c2382cccc3d27/test/file/folder/搜索框.png -------------------------------------------------------------------------------- /test/file/here.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 2016-03-02 18:05 3 | * @author vivaxy 4 | */ 5 | 6 | 'use strict'; 7 | 8 | let db = { 9 | tobi: { 10 | name: 'tobi', 11 | age: 21 12 | }, 13 | loki: { 14 | name: 'loki', 15 | age: 26 16 | }, 17 | jane: { 18 | name: 'jane', 19 | age: 18 20 | } 21 | }; 22 | 23 | module.exports = [ 24 | { 25 | method: 'get', 26 | path: '/pets', 27 | data () { 28 | let names = Object.keys(db); 29 | return names.map((name) => { 30 | return db[name]; 31 | }); 32 | } 33 | }, 34 | { 35 | method: 'get', 36 | path: '/pets/:name', 37 | data () { 38 | let name = this.params.name; 39 | let pet = db[name]; 40 | if (!pet) { 41 | return { 42 | error: `cannot find pet ${name}` 43 | }; 44 | } else { 45 | return pet; 46 | } 47 | } 48 | } 49 | ]; 50 | -------------------------------------------------------------------------------- /test/file/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | serve here 7 | 8 | 9 | 10 |

html ok

11 |

12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/file/js/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 150201 11:50 3 | * @author vivaxy 4 | */ 5 | 6 | 'use strict'; 7 | 8 | document.body.innerHTML += '

js ok

'; 9 | 10 | var ajax = function () { 11 | var req = new XMLHttpRequest(); 12 | req.open('GET', './mock-server/ajax', true); 13 | req.addEventListener('readystatechange', function () { 14 | if (req.readyState === 4 && req.status === 200) { 15 | var response = JSON.parse(req.responseText); 16 | console.log(response); 17 | } 18 | }); 19 | req.send(); 20 | }; 21 | 22 | let image = new Image(); 23 | image.src = 'folder/icon-search.png?_=' + new Date().getTime(); 24 | document.body.appendChild(image); 25 | -------------------------------------------------------------------------------- /test/file/mock-server/ajax: -------------------------------------------------------------------------------- 1 | { 2 | "code": 200, 3 | "msg": "" 4 | } -------------------------------------------------------------------------------- /test/file/mock-server/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": 200 3 | } -------------------------------------------------------------------------------- /test/test/commander.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 2016-08-20 15:03 3 | * @author vivaxy 4 | */ 5 | 6 | 'use strict'; 7 | 8 | let program = require('commander'); 9 | 10 | function range (val) { 11 | return val.split('..').map(Number); 12 | } 13 | 14 | function list (val) { 15 | return val.split(','); 16 | } 17 | 18 | function collect (val, memo) { 19 | memo.push(val); 20 | return memo; 21 | } 22 | 23 | function increaseVerbosity (v, total) { 24 | return total + 1; 25 | } 26 | 27 | function formatWatch (value) { 28 | if (isNaN(Number(value))) { 29 | value = 0; 30 | } else if (value < 0) { 31 | value = 0; 32 | } 33 | return value; 34 | } 35 | 36 | program 37 | .version('0.0.1') 38 | .usage('[options] ') 39 | .option('-w, --watch [interval]', 'will watch files; once changed, reload pages', formatWatch) 40 | .option('-i, --integer ', 'An integer argument', parseInt) 41 | .option('-f, --float ', 'A float argument', parseFloat) 42 | .option('-r, --range ..', 'A range', range) 43 | .option('-l, --list ', 'A list', list) 44 | .option('-o, --optional [value]', 'An optional value') 45 | .option('-c, --collect [value]', 'A repeatable value', collect, []) 46 | .option('-v, --verbose', 'A value that can be increased', increaseVerbosity, 0) 47 | .parse(process.argv); 48 | 49 | // console.log(' int: %j', program.integer); 50 | // console.log(' float: %j', program.float); 51 | // console.log(' optional: %j', program.optional); 52 | // program.range = program.range || []; 53 | // console.log(' range: %j..%j', program.range[0], program.range[1]); 54 | // console.log(' list: %j', program.list); 55 | // console.log(' collect: %j', program.collect); 56 | console.log(' verbosity: %j', program.verbose); 57 | // console.log(' args: %j', program.args); 58 | // 59 | console.log(' watch', program.watch); 60 | 61 | --------------------------------------------------------------------------------