├── .npmignore ├── index.js ├── pages ├── 404.html ├── init.html ├── 50x.html └── dir.html ├── serve ├── index.js ├── out │ ├── JsonOut.js │ ├── JsonpOut.js │ ├── Base.js │ └── ServerSent.js ├── package.json ├── README.md ├── Route.js ├── index.d.ts └── package-lock.json ├── .eslintrc ├── lib ├── util │ ├── IP.js │ ├── PORT.js │ ├── resp.d.ts │ ├── HOSTS.js │ ├── compressor.d.ts │ ├── build.js │ ├── misc.js │ ├── minify.js │ ├── compressor.js │ └── resp.js ├── conf │ ├── F2E_CONFIG.js │ └── index.js ├── middleware │ ├── build.js │ ├── namehash.js │ ├── try_files.js │ ├── less.js │ ├── livereload.js │ ├── include.js │ └── index.js ├── apps │ ├── static.js │ └── memory-tree │ │ └── index.js └── server │ └── index.js ├── start.js ├── .editorconfig ├── LICENSE ├── .gitignore ├── .f2econfig.js ├── package.json ├── bin └── f2e ├── CHANGELOG.md ├── README.md └── index.d.ts /.npmignore: -------------------------------------------------------------------------------- 1 | serve/ 2 | docs/ 3 | output/ -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/server/index') 2 | -------------------------------------------------------------------------------- /pages/404.html: -------------------------------------------------------------------------------- 1 | 2 |

404: <%-pathname%> is gone!

-------------------------------------------------------------------------------- /pages/init.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

服务器启动中。。。

4 | -------------------------------------------------------------------------------- /pages/50x.html: -------------------------------------------------------------------------------- 1 | 2 |

500: ServerError

3 | 4 |
<%=error && error.toString()%>
-------------------------------------------------------------------------------- /serve/index.js: -------------------------------------------------------------------------------- 1 | exports.Route = require('./Route') 2 | exports.out = { 3 | Base: require('./out/Base'), 4 | JsonOut: require('./out/JsonOut'), 5 | JsonpOut: require('./out/JsonpOut'), 6 | ServerSent: require('./out/ServerSent') 7 | } 8 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard", 3 | "env": { 4 | "es2021": true 5 | }, 6 | "plugins": [ 7 | "standard" 8 | ], 9 | 10 | "rules": { 11 | "indent": ["error", 4], 12 | "camelcase": "off" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/util/IP.js: -------------------------------------------------------------------------------- 1 | const IFS = require('os').networkInterfaces() 2 | module.exports = (Object.keys(IFS) 3 | .map( 4 | x => IFS[x].filter( 5 | x => x.family === 'IPv4' && !x.internal 6 | )[0] 7 | ) 8 | .filter(x => x)[0] || {address: '127.0.0.1'}).address 9 | -------------------------------------------------------------------------------- /start.js: -------------------------------------------------------------------------------- 1 | // require('.')({ 2 | // host: 'server.local', 3 | // port: '9999', 4 | // root: './lib', 5 | // try_files: 'apps/static.js', 6 | // onServerCreate: function (server, conf) { 7 | // console.log('server:', server) 8 | // console.log('conf:', conf) 9 | // } 10 | // }) 11 | 12 | console.log(process.env.NIHAO); 13 | -------------------------------------------------------------------------------- /lib/util/PORT.js: -------------------------------------------------------------------------------- 1 | const tcpPortUsed = require('tcp-port-used') 2 | const IP = require('./IP') 3 | const PORT = 2850 4 | 5 | /** 6 | * @returns {Promise} 7 | */ 8 | const getPort = () => new Promise((resolve, reject) => { 9 | function getPort (port) { 10 | tcpPortUsed 11 | .check(port, IP) 12 | .then( 13 | inUse => inUse ? getPort(++port) : resolve(port), 14 | err => reject(err) 15 | ) 16 | } 17 | getPort(PORT) 18 | }) 19 | module.exports = getPort 20 | -------------------------------------------------------------------------------- /lib/conf/F2E_CONFIG.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | let config = null 5 | const F2E_CONFIG = '.f2econfig.js' 6 | 7 | exports.F2E_CONFIG = F2E_CONFIG 8 | exports.setConfigPath = (c) => { config = c } 9 | exports.getConfigPath = () => { 10 | if (!config) { 11 | // 没有提供配置文件路径, 但是启动目录如果有同名文件,使用此配置(兼容之前版本) 12 | const pathConf = path.join(process.cwd(), F2E_CONFIG) 13 | if (fs.existsSync(pathConf)) { 14 | return F2E_CONFIG 15 | } 16 | } 17 | return config 18 | } 19 | -------------------------------------------------------------------------------- /serve/out/JsonOut.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const createRespUtil = require('f2e-server/lib/util/resp') 3 | 4 | /** 5 | * @type {import('../index').ExecOut} 6 | */ 7 | const provider = (fn, conf = {}) => (req, resp) => { 8 | const RespUtil = createRespUtil(conf) 9 | Promise.resolve(fn(req, resp, conf)).then(data => { 10 | RespUtil.handleSuccess(req, resp, '.json', data && JSON.stringify(data)) 11 | }).catch(err => { 12 | console.log(err) 13 | RespUtil.handleError(resp, err, req) 14 | }) 15 | return false 16 | } 17 | module.exports = provider 18 | -------------------------------------------------------------------------------- /serve/out/JsonpOut.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const createRespUtil = require('f2e-server/lib/util/resp') 3 | /** 4 | * @type {import('../index').ExecOut} 5 | */ 6 | const provider = (fn, conf = {}) => (req, resp) => { 7 | const RespUtil = createRespUtil(conf) 8 | const { callback = 'callback' } = req['data'] 9 | Promise.resolve(fn(req, resp, conf)).then(data => { 10 | RespUtil.handleSuccess(req, resp, '.js', `${callback}(${JSON.stringify(data)})`) 11 | }).catch(err => { 12 | console.log(err) 13 | RespUtil.handleError(resp, err, req) 14 | }) 15 | return false 16 | } 17 | 18 | module.exports = provider 19 | -------------------------------------------------------------------------------- /serve/out/Base.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const createRespUtil = require('f2e-server/lib/util/resp') 3 | 4 | /** 5 | * @param {string} type 6 | * @returns {import('../').ExecOut} 7 | */ 8 | const provider = (type = 'text/html') => { 9 | return (fn, conf = {}) => (req, resp) => { 10 | const RespUtil = createRespUtil(conf) 11 | Promise.resolve(fn(req, resp, conf)).then(data => { 12 | RespUtil.handleSuccess(req, resp, type, data) 13 | }).catch(err => { 14 | console.log(err) 15 | RespUtil.handleError(resp, err, req) 16 | }) 17 | return false 18 | } 19 | } 20 | module.exports = provider 21 | -------------------------------------------------------------------------------- /lib/util/resp.d.ts: -------------------------------------------------------------------------------- 1 | import { Stats } from 'fs' 2 | import { ServerResponse, IncomingMessage } from 'http'; 3 | import { F2EConfig } from '../../index' 4 | 5 | export interface RespUtil { 6 | handleError: (resp: ServerResponse, error: Error, req?: IncomingMessage) => ServerResponse; 7 | handleSuccess: (req: IncomingMessage, resp: ServerResponse, pathname?: string, data?: string | Buffer | Stats | MemoryTree.DataBuffer) => ServerResponse; 8 | handleNotFound: (req: IncomingMessage, resp: ServerResponse, pathname?: string) => ServerResponse; 9 | handleDirectory: (req: IncomingMessage, resp: ServerResponse, pathname: string, store: any) => string | false; 10 | } 11 | 12 | declare let mod: (conf: F2EConfig) => RespUtil 13 | export = mod 14 | -------------------------------------------------------------------------------- /lib/util/HOSTS.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const os = require('os') 3 | 4 | const hostsPath = os.type().match(/Windows/) ? 'C:\\Windows\\System32\\drivers\\etc\\hosts' : '/etc/hosts' 5 | 6 | const getHosts = r => fs.readFileSync(hostsPath) 7 | 8 | module.exports = (host = 'localhost') => { 9 | if (typeof host !== 'string') { 10 | console.log('hostname is needed!') 11 | } 12 | 13 | let res = getHosts().toString() 14 | if (host === 'reset') { 15 | res = res.replace(/[\n\r]?127\.0\.0\.1[^\n\r]+/g, '\n127.0.0.1 localhost') 16 | console.info('hostname reset ok!') 17 | } else if (res.indexOf(' ' + host) === -1) { 18 | res += '\n127.0.0.1 ' + host 19 | } 20 | fs.writeFile(hostsPath, res, err => err && console.log(err)) 21 | } 22 | -------------------------------------------------------------------------------- /lib/util/compressor.d.ts: -------------------------------------------------------------------------------- 1 | import { Transform } from "stream"; 2 | import { InputType } from "zlib"; 3 | import { IncomingMessage } from 'http'; 4 | import { F2EConfig } from '../../index' 5 | 6 | export interface Compressor { 7 | createStream: () => Transform, 8 | compressSync: (buf: InputType) => Buffer, 9 | contentEncoding: E 10 | } 11 | 12 | export type PredefinedCompressorType = 'br' | 'gzip' | 'deflate' 13 | 14 | export type CompressorType = Compressor | PredefinedCompressorType 15 | 16 | export type PredefinedCompressors = Record 17 | 18 | declare const predefinedCompressors: PredefinedCompressors 19 | declare const getCompressor: (req: IncomingMessage, conf: F2EConfig) => Compressor 20 | export { predefinedCompressors, getCompressor } 21 | -------------------------------------------------------------------------------- /serve/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "f2e-serve", 3 | "version": "0.7.1", 4 | "description": "serve utils for f2e-server", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/shy2850/f2e-server.git" 12 | }, 13 | "keywords": [ 14 | "f2e-server" 15 | ], 16 | "author": "shy2850", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/shy2850/f2e-server/issues" 20 | }, 21 | "homepage": "https://github.com/shy2850/f2e-server#readme", 22 | "dependencies": { 23 | "f2e-server": "^2.20.13", 24 | "mime": "^3.0.0" 25 | }, 26 | "devDependencies": { 27 | "@types/mime": "^3.0.1", 28 | "@types/node": "^18.11.18" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | 9 | [vcbuild.bat] 10 | end_of_line = crlf 11 | 12 | [{lib,src,test}/**.js] 13 | indent_style = space 14 | indent_size = 4 15 | 16 | [src/**.{h,cc}] 17 | indent_style = space 18 | indent_size = 4 19 | 20 | [test/*.py] 21 | indent_style = space 22 | indent_size = 4 23 | 24 | [configure] 25 | indent_style = space 26 | indent_size = 4 27 | 28 | [Makefile] 29 | indent_style = tab 30 | indent_size = 8 31 | 32 | [{deps,tools}/**] 33 | indent_style = ignore 34 | indent_size = ignore 35 | end_of_line = ignore 36 | trim_trailing_whitespace = ignore 37 | charset = ignore 38 | 39 | [{test/fixtures,deps,tools/eslint,tools/gyp,tools/icu,tools/msvs}/**] 40 | insert_final_newline = false 41 | -------------------------------------------------------------------------------- /serve/README.md: -------------------------------------------------------------------------------- 1 | # f2e-serve 2 | f2e-server 的服务端开发工具 3 | 4 | ## 基本用法 5 | 6 | 7 | ```js 8 | import { MiddlewareCreater } from 'f2e-server' 9 | import { Route, out } from 'f2e-serve' 10 | import * as fs from 'fs' 11 | 12 | // 以f2e-server中间件的模式构建模块 13 | const creater: MiddlewareCreater = (conf) => { 14 | const dosomething = async (req) => { 15 | return { success: true, data: req.data } 16 | } 17 | const download = () => fs.readFileSync('xx.pdf') 18 | 19 | const route = new Route() 20 | route.on('api/dosomething', out.JsonOut(dosomething, conf)); // 普通json接口返回 21 | route.on('api/dosomething.js', out.JsonpOut(dosomething, conf)); // 支持callback参数的jsonp接口返回 22 | route.on('api/doingsomething', out.ServerSent(dosomething, { ...conf, interval: 2000 })); // 支持serverSent每2000ms一次推送 23 | route.on('xx.pdf', out.Base('application/octet-stream')(download, conf)) // 支持原始数据输出 24 | 25 | return { 26 | onRoute: route.execute 27 | } 28 | } 29 | export default creater 30 | ``` -------------------------------------------------------------------------------- /lib/middleware/build.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const _ = require('lodash') 3 | const emptyFn = () => 1 4 | 5 | /** 6 | * @type {import('../../index').MiddlewareCreater} 7 | */ 8 | module.exports = (conf) => { 9 | const { 10 | output, 11 | build, 12 | shouldUseMinify = emptyFn 13 | } = conf 14 | if (!output) { 15 | return 16 | } 17 | return { 18 | async onSet (pathname, data, store) { 19 | if (_.isPlainObject(data)) { 20 | return data 21 | } 22 | if (!build || !data || !shouldUseMinify(pathname, data)) { 23 | return data 24 | } 25 | return require('../util/minify').execute(pathname, data) 26 | }, 27 | onRoute (pathname, req, resp, memory) { 28 | if (pathname === 'server-build-output') { 29 | require('../util/build')(() => { 30 | resp.end('build ok!') 31 | }) 32 | return false 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 云香水识 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 | -------------------------------------------------------------------------------- /serve/Route.js: -------------------------------------------------------------------------------- 1 | const test = (reg, str) => { 2 | if (reg instanceof RegExp) { 3 | return reg.test(str) 4 | } else { 5 | return reg === str 6 | } 7 | } 8 | module.exports = class { 9 | constructor () { 10 | this.routes = [] 11 | this.execute = this.execute.bind(this) 12 | this.on = this.on.bind(this) 13 | } 14 | execute (pathname, req, resp, memory) { 15 | const {routes} = this 16 | let matches = false 17 | for (let i = 0; i < routes.length; i++) { 18 | const route = routes[i] 19 | if (test(route.reg, pathname)) { 20 | matches = true 21 | const res = route.exec(req, resp, pathname, memory) 22 | if (typeof res !== 'undefined') { 23 | return res 24 | } 25 | } 26 | } 27 | if (matches) { 28 | return false 29 | } 30 | } 31 | on (reg, exec) { 32 | this.routes.push({reg, exec}) 33 | } 34 | match (pathname) { 35 | return this.routes.some(route => test(route.reg, pathname)) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/util/build.js: -------------------------------------------------------------------------------- 1 | const Middleware = require('../middleware/index') 2 | const MemoryTree = require('memory-tree').default 3 | const emptyFn = () => console.log('build finished!') 4 | const { renderConf } = require('../conf') 5 | module.exports = (callback = emptyFn, opt = {watch: 0, timeout: 1000}) => { 6 | const conf = renderConf({build: true}) 7 | const middleware = Middleware(conf) 8 | const memory = MemoryTree({ 9 | root: conf.root, 10 | watch: !!opt.watch, 11 | dest: conf.output, 12 | onSet: middleware.onSet, 13 | onGet: middleware.onGet, 14 | buildWatcher: (pathname, type, build) => { 15 | middleware.buildWatcher(pathname, type, build) 16 | memory.store.onBuildingChange(function (building) { 17 | if (!build) { 18 | memory.output('') 19 | } 20 | }) 21 | }, 22 | buildFilter: middleware.buildFilter, 23 | outputFilter: middleware.outputFilter 24 | }) 25 | if (conf.output) { 26 | memory.input('').then(() => { 27 | memory.output('') 28 | }).catch(err => console.trace(err)) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.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 (http://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 | .vscode 60 | .DS_Store 61 | output 62 | node_modules.tar.gz 63 | -------------------------------------------------------------------------------- /serve/index.d.ts: -------------------------------------------------------------------------------- 1 | import { IncomingMessage, ServerResponse } from "http" 2 | import { F2EConfig, RequestWith } from 'f2e-server' 3 | import { MemoryTree } from "memory-tree" 4 | 5 | type onRoute = Required['onRoute'] 6 | 7 | export interface ExecFn { 8 | (req: RequestWith, resp: ServerResponse, pathname?: string, memory?: MemoryTree.Store): ReturnType 9 | } 10 | export interface ServerAPI { 11 | (req: RequestWith, resp: ServerResponse): F 12 | } 13 | export interface BaseOutConfig extends Partial { 14 | interval?: number 15 | interval_beat?: number 16 | } 17 | export interface ExecOut { 18 | (fn: ServerAPI, conf?: BaseOutConfig): ExecFn 19 | } 20 | 21 | export interface Out { 22 | Base: (type: string) => ExecOut 23 | JsonOut: ExecOut 24 | JsonpOut: ExecOut 25 | ServerSent: ExecOut 26 | } 27 | 28 | export class Route { 29 | execute: onRoute 30 | on: { 31 | (reg: string | RegExp, exec: ExecFn): void 32 | } 33 | match: { 34 | (pathname: string): boolean 35 | } 36 | } 37 | export declare const out: Out 38 | -------------------------------------------------------------------------------- /lib/util/misc.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const mime = require('mime') 3 | 4 | /** 5 | * 6 | * @param {string} pathname 7 | * @returns {boolean} 8 | */ 9 | const isText = pathname => { 10 | const type = mime.getType(pathname) 11 | return /\b(html?|txt|javascript|json)\b/.test(type || 'exe') 12 | } 13 | 14 | /** 15 | * 16 | * @param {string} str 17 | * @returns {string} 18 | */ 19 | const decode = str => { 20 | try { 21 | return decodeURIComponent(str) 22 | } catch (e) { 23 | return str 24 | } 25 | } 26 | /** 27 | * 28 | * @param { URLSearchParams } searchParams 29 | */ 30 | function queryparam (searchParams) { 31 | /** 32 | * @type {Record} 33 | */ 34 | let params = {} 35 | searchParams.forEach((v, k) => { 36 | if (params[k]) { 37 | // @ts-ignore 38 | params[k] = [].concat(params[k]).concat(v) 39 | } else { 40 | params[k] = v 41 | } 42 | }) 43 | return params 44 | } 45 | const pathname_fixer = (str = '') => (str.match(/[^/\\]+/g) || []).join('/') 46 | const pathname_dirname = (str = '') => (str.match(/[^/\\]+/g) || []).slice(0, -1).join('/') 47 | 48 | module.exports = { 49 | isText, decode, queryparam, pathname_fixer, pathname_dirname 50 | } 51 | -------------------------------------------------------------------------------- /.f2econfig.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const { argv } = process 4 | const build = process.env['NODE_ENV'] === 'build' || argv[argv.length - 1] === 'build' 5 | const { join } = require('path') 6 | 7 | /** 8 | * @type {import('./index').F2EConfig} 9 | */ 10 | const config = { 11 | livereload: !build, 12 | build, 13 | // app: 'static', 14 | // __withlog__: true, 15 | gzip: true, 16 | // compressors: ['br', 'gzip', 'deflate'], 17 | useLess: false, 18 | shouldUseMinify: (pathname) => { 19 | // 使用include压缩,无需处理 20 | return pathname != 'test/libs.js' 21 | }, 22 | middlewares: [ 23 | // { middleware: 'template', test: /\.html?/ }, 24 | // () => { 25 | // return { 26 | // onRoute: p => { 27 | // if (!p) return 'index.html' 28 | // }, 29 | // } 30 | // } 31 | ], 32 | // try_files: 'index.html', 33 | output: join(__dirname, './output'), 34 | // onServerCreate: (server) => { 35 | // const { Server } = require('ws') 36 | // const wss = new Server({server}); 37 | // wss.on('connection', (socket) => { 38 | // socket.send('init') 39 | // }) 40 | // } 41 | // authorization: 'admin:admin' 42 | } 43 | module.exports = config 44 | -------------------------------------------------------------------------------- /lib/util/minify.js: -------------------------------------------------------------------------------- 1 | 2 | const getUtils = () => { 3 | try { 4 | const CleanCSS = require('clean-css') 5 | const cleanCss = new CleanCSS({ compatibility: '*' }) 6 | 7 | const uglifyJs = require('uglify-js') 8 | const uglifyEs = require('terser') 9 | 10 | return { 11 | CleanCSS, 12 | cleanCss, 13 | uglifyJs, 14 | uglifyEs 15 | } 16 | } catch (e) { 17 | console.log('\nbuild 模式需要安装依赖:\n\t npm i clean-css uglify-js terser --save-dev\n') 18 | process.exit(1) 19 | } 20 | } 21 | 22 | /** 23 | * @param {string} pathname 24 | * @param {string | {} | Buffer} data 25 | * @returns {Promise} 26 | */ 27 | exports.execute = async (pathname, data) => { 28 | const { 29 | cleanCss, 30 | uglifyJs, 31 | uglifyEs 32 | } = getUtils() 33 | const extType = (pathname.match(/\.(\w+)$/) || [])[1] 34 | switch (extType) { 35 | case 'js': 36 | let js = await uglifyEs.minify(data && data.toString()) 37 | if (!js.code) { 38 | js = uglifyJs.minify(data && data.toString()) 39 | } 40 | return js.code 41 | case 'css': 42 | let css = cleanCss.minify(data && data.toString()) 43 | return css.styles 44 | default: 45 | return data 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/middleware/namehash.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const path = require('path') 3 | const { pathname_fixer } = require('../util/misc') 4 | 5 | /** 6 | * @type {import('../../index').MiddlewareCreater} 7 | */ 8 | module.exports = (conf) => { 9 | const namehash = Object.assign({ 10 | entries: ['index\\.html$'], 11 | searchValue: ['\\s(?:src)="([^"]*?)"', '\\s(?:href)="([^"]*?)"'], 12 | replacer: (output, hash) => `/${output}?${hash}` 13 | }, conf.namehash || {}) 14 | 15 | const needhash = new RegExp(namehash.entries.join('|')) 16 | const searchValues = namehash.searchValue.map(t => new RegExp(t, 'g')) 17 | return { 18 | async onGet (pathname, data, memory, map) { 19 | if (data && needhash.test(pathname)) { 20 | let result = data.toString() 21 | for (let i = 0; i < searchValues.length; i++) { 22 | const searchValue = searchValues[i] 23 | result = result.replace(searchValue, function (_, a) { 24 | const p = pathname_fixer(path.join(path.dirname(pathname), a)) 25 | const out = map.get(p) 26 | return out ? _.replace(a, () => namehash.replacer(out.output, out.hash)) : _ 27 | }) 28 | } 29 | return result 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /serve/out/ServerSent.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @type {import('../index').ExecOut} 5 | */ 6 | const provider = (fn, conf = { 7 | interval: 1000, 8 | interval_beat: 30000 9 | }) => (req, resp) => { 10 | const { renderHeaders = h => h } = conf 11 | resp.writeHead(200, renderHeaders({ 12 | 'Content-Type': 'text/event-stream', 13 | 'Cache-Control': 'no-cache', 14 | 'Connection': 'keep-alive' 15 | }, req)) 16 | 17 | let interval1 18 | let interval2 19 | 20 | const heartBeat = function heartBeat () { 21 | if (resp.writable && !resp.writableEnded) { 22 | resp.write(`data:1\n\n`) 23 | interval1 = setTimeout(heartBeat, conf.interval_beat || 30000) 24 | } 25 | } 26 | 27 | const loop = async function loop () { 28 | try { 29 | const res = await Promise.resolve(fn(req, resp, conf)) 30 | if (res && resp.writable && !resp.writableEnded) { 31 | resp.write(`data:${JSON.stringify(res)}\n\n`) 32 | } 33 | } catch (e) { 34 | console.log(e) 35 | } 36 | if (conf.interval) { 37 | interval2 = setTimeout(loop, conf.interval || 1000) 38 | } else { 39 | heartBeat() 40 | } 41 | } 42 | 43 | req.socket.addListener('close', () => { 44 | clearTimeout(interval1) 45 | clearTimeout(interval2) 46 | resp.end() 47 | }) 48 | loop() 49 | return false 50 | } 51 | 52 | module.exports = provider 53 | -------------------------------------------------------------------------------- /lib/util/compressor.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const zlib = require('zlib') 4 | 5 | /** 6 | * @type {import('./compressor').PredefinedCompressors} 7 | */ 8 | const predefinedCompressors = { 9 | br: { 10 | createStream: zlib.createBrotliCompress, 11 | compressSync: zlib.brotliCompressSync, 12 | contentEncoding: 'br' 13 | }, 14 | gzip: { 15 | createStream: zlib.createGzip, 16 | compressSync: zlib.gzipSync, 17 | contentEncoding: 'gzip' 18 | }, 19 | deflate: { 20 | createStream: zlib.createDeflate, 21 | compressSync: zlib.deflateSync, 22 | contentEncoding: 'deflate' 23 | } 24 | } 25 | 26 | /** 27 | * Get the most suitable compression algorithm for the given request 28 | * @param {import('http').IncomingMessage} req 29 | * @param {import('../../index').F2EConfig} conf 30 | * @return {import('./compressor').Compressor?} 31 | */ 32 | const getCompressor = (req, conf) => { 33 | let { gzip, compressors } = conf 34 | const encodingStr = req.headers['accept-encoding'] ? req.headers['accept-encoding'].toString() : '' 35 | const encodings = encodingStr.split(/, ?/).map(x => x.split(';')[0]) 36 | const _compressors = compressors ? compressors.map(x => { 37 | if (typeof x === 'string') { 38 | return predefinedCompressors[x] 39 | } else { 40 | return x 41 | } 42 | }) : [] 43 | if (gzip) { 44 | _compressors.push(predefinedCompressors.gzip) 45 | } 46 | for (const compressor of _compressors) { 47 | if (encodings.includes(compressor.contentEncoding)) { 48 | return compressor 49 | } 50 | } 51 | return null 52 | } 53 | 54 | module.exports = { predefinedCompressors, getCompressor } 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "f2e-server", 3 | "version": "2.20.13", 4 | "description": "f2e-server 2", 5 | "main": "index.js", 6 | "files": [ 7 | "bin", 8 | "keys", 9 | "lib", 10 | "pages", 11 | ".f2econfig.js", 12 | "index.*" 13 | ], 14 | "scripts": { 15 | "start": "node ./bin/f2e start -O", 16 | "start:config": "node ./bin/f2e start -c .f2econfig.1.js", 17 | "build": "node ./bin/f2e build", 18 | "test": "node test/index.js", 19 | "lint": "eslint lib/**/**.js --fix", 20 | "prepublishOnly": "npm run lint" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/shy2850/f2e-server.git" 25 | }, 26 | "keywords": [ 27 | "f2e", 28 | "f2e-server", 29 | "proxy", 30 | "debug", 31 | "runtime-build" 32 | ], 33 | "author": "shy2850", 34 | "license": "MIT", 35 | "bugs": { 36 | "url": "https://github.com/shy2850/f2e-server/issues" 37 | }, 38 | "bin": { 39 | "f2e": "./bin/f2e" 40 | }, 41 | "typings": "index.d.ts", 42 | "homepage": "https://github.com/shy2850/f2e-server#readme", 43 | "devDependencies": { 44 | "clean-css": "^4.2.3", 45 | "eslint": "^8.39.0", 46 | "eslint-config-standard": "^10.2.1", 47 | "eslint-plugin-import": "^2.22.1", 48 | "eslint-plugin-node": "^4.2.3", 49 | "eslint-plugin-promise": "^3.8.0", 50 | "eslint-plugin-standard": "^3.1.0", 51 | "f2e-middle-template": "0.3.0", 52 | "less": "^4.1.3", 53 | "marked": "^4.3.0", 54 | "terser": "^5.15.1", 55 | "uglify-js": "^3.17.4", 56 | "ws": "^7.4.3" 57 | }, 58 | "dependencies": { 59 | "@types/less": "^3.0.2", 60 | "chokidar": "^3.5.3", 61 | "commander": "^2.20.3", 62 | "etag": "^1.8.1", 63 | "lodash": "^4.17.21", 64 | "memory-tree": "^0.6.23", 65 | "mime": "^3.0.0", 66 | "tcp-port-used": "^1.0.2" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lib/middleware/try_files.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | 3 | /** 4 | * @type {import('../../index').MiddlewareCreater} 5 | */ 6 | module.exports = conf => { 7 | const { 8 | renderHeaders = h => h, 9 | try_files 10 | } = conf 11 | 12 | if (!try_files) { 13 | return null 14 | } 15 | 16 | /** 17 | * @type {import('../../index').TryFilesItem[]} 18 | */ 19 | let tries = [] 20 | if (typeof try_files === 'string') { 21 | tries.push({ 22 | test: /.*/, 23 | index: try_files 24 | }) 25 | } else { 26 | tries = try_files 27 | } 28 | return { 29 | onRoute: (pathname, req, resp, memory) => { 30 | for (let i = 0; i < tries.length; i++) { 31 | const item = tries[i] 32 | if (item.test.test(pathname)) { 33 | let p = pathname 34 | if (item.replacer) { 35 | p = pathname.replace(item.test, item.replacer) 36 | } 37 | let data = memory._get(p) 38 | if (_.isPlainObject(data)) { 39 | p += '/' + item.index 40 | data = memory._get(p) 41 | } 42 | if (!data) { 43 | if (item.location) { 44 | let location = typeof item.location === 'string' ? item.location : item.location(pathname, req, resp, memory) 45 | resp.writeHead(302, renderHeaders({ 46 | location 47 | }, req)) 48 | resp.end() 49 | return false 50 | } else { 51 | return typeof item.index === 'string' ? item.index : item.index(pathname, req, resp, memory) 52 | } 53 | } else { 54 | return p 55 | } 56 | } 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/apps/static.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { URL } = require('url') 3 | const fs = require('fs') 4 | const path = require('path') 5 | const Resp = require('../util/resp') 6 | const { pathname_fixer, decode, queryparam } = require('../util/misc') 7 | 8 | /** 9 | * 10 | * @param {import('../../index').F2EConfig} conf 11 | * @returns 12 | */ 13 | const render = conf => { 14 | const { root, __withlog__, range_size, beforeRoute, buildFilter } = conf 15 | const { 16 | handleSuccess, 17 | handleNotFound, 18 | handleDirectory 19 | } = Resp(conf) 20 | 21 | /** 22 | * 23 | * @param {import('http').IncomingMessage} req 24 | * @param {import('http').ServerResponse} resp 25 | * @returns 26 | */ 27 | const fn = (req, resp) => { 28 | const location = new URL('http://127.0.0.1' + req.url) 29 | let pathname = pathname_fixer(decode(location.pathname)) 30 | req['data'] = queryparam(location.searchParams) 31 | 32 | if (beforeRoute) { 33 | let routeResult = beforeRoute(pathname, req, resp, conf) 34 | if (routeResult === false) { 35 | return 36 | } else { 37 | pathname = routeResult || pathname 38 | } 39 | } 40 | 41 | if (!buildFilter(pathname)) { 42 | handleNotFound(req, resp, pathname) 43 | } 44 | 45 | let pathname_real = path.join(root, pathname) 46 | fs.stat(pathname_real, (error, stats) => { 47 | if (__withlog__) { 48 | console.log(`${new Date().toLocaleString()}: ${pathname}`, error || '') 49 | } 50 | 51 | if (stats && stats.isFile()) { 52 | if (stats.size > range_size) { 53 | handleSuccess(req, resp, pathname, stats) 54 | } else { 55 | handleSuccess(req, resp, pathname, fs.readFileSync(pathname_real)) 56 | } 57 | } else if (stats && stats.isDirectory()) { 58 | const data = handleDirectory(req, resp, pathname, fs.readdirSync(pathname_real).filter(buildFilter).reduce((m, n) => { 59 | return Object.assign(m, {[n]: 1}) 60 | }, {})) 61 | if (data) { 62 | handleSuccess(req, resp, pathname, data) 63 | } 64 | } else { 65 | handleNotFound(req, resp, pathname) 66 | } 67 | }) 68 | } 69 | return fn 70 | } 71 | 72 | module.exports = render 73 | -------------------------------------------------------------------------------- /bin/f2e: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict' 3 | const path = require('path') 4 | const program = require('commander') 5 | const base = require('../package.json') 6 | const fs = require('fs') 7 | const { setConfigPath, F2E_CONFIG } = require('../lib/conf/F2E_CONFIG') 8 | setConfigPath(F2E_CONFIG) 9 | program 10 | .version(base.version) 11 | 12 | program 13 | .command('conf') 14 | .description(`生成 ${F2E_CONFIG} 配置文件模板`) 15 | .action(() => { 16 | fs.readFile(path.join(__dirname, '../', F2E_CONFIG), function (err, data) { 17 | !err && fs.writeFile(path.resolve(F2E_CONFIG), data.toString().replace(`import('./index')`, `import('f2e-server')`), function (err) { 18 | err && console.log(err) 19 | }) 20 | }) 21 | }) 22 | program 23 | .command('build') 24 | .option('-w, --watch ', 'build with watching-update') 25 | .option('-c, --config ', `config file. default to ${F2E_CONFIG}`) 26 | .option('-t, --timeout ', 'build-in with timeout(1000ms) after build-out') 27 | .description('编译输出') 28 | .action(({ watch, timeout, config }) => { 29 | timeout = (timeout | 0) || 1000 30 | let beginTime = Date.now() + timeout 31 | console.log('build begin...') 32 | if (config) { 33 | setConfigPath(config) 34 | } 35 | require('../lib/util/build')(function () { 36 | console.log('build finished' + (watch ? '!\n & building...' : ' with: ' + (Date.now() - beginTime) + 'ms')) 37 | }, {watch, timeout}) 38 | }) 39 | program 40 | .command('start') 41 | .option('-p, --port ', 'http server port') 42 | .option('-c, --config ', `config file. default to ${F2E_CONFIG}`) 43 | .option('-H, --host ', 'port 80 & set local host') 44 | .option('-B, --build ', 'build with true') 45 | .option('-O, --open', '打开浏览器') 46 | .description('启动服务器') 47 | .action(({ port, config, host, build, open }) => { 48 | if (config) { 49 | setConfigPath(config) 50 | } 51 | require('../index')({ 52 | port: port ? Number(port) : undefined, 53 | host, 54 | build: !!build, 55 | open: !!open 56 | }) 57 | }) 58 | 59 | program 60 | .command('create ') 61 | .description('一键创建项目模板') 62 | .action(function (appname = 'myApp') { 63 | require('child_process') 64 | .exec(`git clone https://gitee.com/f2e-server/f2e-react-app.git ${appname}`, function () { 65 | console.log('初始化完成!') 66 | }) 67 | }) 68 | 69 | // 开始解析用户输入的命令 70 | program.parse(process.argv) 71 | -------------------------------------------------------------------------------- /lib/conf/index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const path = require('path') 3 | const fs = require('fs') 4 | const { getConfigPath } = require('./F2E_CONFIG') 5 | const _ = require('lodash') 6 | const { exit } = require('process') 7 | const { isText } = require('../util/misc') 8 | 9 | /** 10 | * @type {import('../../index').F2EConfig & { app?: string }} 11 | */ 12 | const defaultConf = { 13 | root: process.cwd(), 14 | livereload: true, 15 | isText, 16 | buildFilter: (pathname) => !/node_modules|([\\/]|^)\./.test(pathname), 17 | shouldUseCompressor: (pathname, size) => isText(pathname) && size > 4096, 18 | shouldUseMinify: () => true, 19 | __ignores__: new Set(), 20 | page_init: fs.readFileSync(path.join(__dirname, '../../pages/init.html')).toString(), 21 | page_404: path.join(__dirname, '../../pages/404.html'), 22 | page_50x: path.join(__dirname, '../../pages/50x.html'), 23 | page_dir: path.join(__dirname, '../../pages/dir.html'), 24 | onServerCreate: () => {}, 25 | app: 'memory-tree' 26 | } 27 | const _renderConf = (cf) => { 28 | let conf = {} 29 | if (fs.existsSync(cf)) { 30 | try { 31 | conf = require(cf) 32 | } catch (e) { 33 | console.error(`${cf} error`, e) 34 | exit(1) 35 | } 36 | } else { 37 | console.info(`\n no\n ${cf}! \n run\n 'f2e conf' `) 38 | } 39 | return Object.assign({}, conf) 40 | } 41 | 42 | /** 43 | * @param {import('../../index').F2EConfig} c 44 | * @returns {import('../../index').F2EConfig} 45 | */ 46 | const renderConf = (c = {}) => { 47 | const root = process.cwd() 48 | Object.keys(c).forEach(k => { 49 | if (typeof c[k] === 'undefined') { 50 | delete c[k] 51 | } 52 | }) 53 | if (c.root && !path.isAbsolute(c.root)) { 54 | c.root = path.join(root, c.root) 55 | } 56 | const configPath = getConfigPath() 57 | const conf = configPath ? _renderConf(path.join(root, configPath)) : { root, livereload: false } 58 | const final = _.clone(_.extend({}, defaultConf, conf, c)) 59 | if (typeof final.app === 'string') { 60 | final.app = require(path.join(__dirname, `../apps/${final.app}`)) 61 | } 62 | return final 63 | } 64 | 65 | /** 66 | * @type {Map} 67 | */ 68 | const map_config = new Map() 69 | 70 | /** 71 | * @param {string} origin 72 | * @param {import('../../index').F2EConfig} conf 73 | */ 74 | const setConfig = (origin, conf) => map_config.set(origin, conf) 75 | const getConfig = (origin = '') => map_config.get(origin) 76 | 77 | module.exports = { 78 | renderConf, 79 | setConfig, 80 | getConfig 81 | } 82 | -------------------------------------------------------------------------------- /lib/middleware/less.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { readdirSync } = require('fs') 3 | const path = require('path') 4 | const _ = require('lodash') 5 | const pathREG = /[^\\/]+/g 6 | 7 | /** 8 | * @type {import('../../index').MiddlewareCreater} 9 | */ 10 | module.exports = conf => { 11 | const { useLess, build } = conf 12 | if (!useLess) { 13 | return 14 | } 15 | 16 | /** @type {LessStatic} */ 17 | let less 18 | try { 19 | less = require('less') 20 | } catch (e) { 21 | console.error('useLess 需要安装less依赖 \n npm i less@4 --save-dev') 22 | process.exit(1) 23 | } 24 | let imports = {} 25 | 26 | return { 27 | onSet: function (pathname, data, store) { 28 | if (_.isPlainObject(data)) { 29 | return data 30 | } 31 | if (/\.less$/.test(pathname)) { 32 | return new Promise((resolve, reject) => { 33 | const outputPath = pathname.replace(/\.less$/, '.css') 34 | const outputMapPath = outputPath.replace(/\.css$/, '.css.map') 35 | const lessStr = (data && data.toString()) || '' 36 | less.render(lessStr.replace(/(@import.*)"(\S*\/)"/g, (impt, pre, dir) => { 37 | let pkg = path.join(path.dirname(pathname), dir) 38 | const d = pkg.match(pathREG) || [] 39 | const p = pathname.match(pathREG) || [] 40 | _.set(imports, [d.join('/'), p.join('/')], 1) 41 | return readdirSync(pkg) 42 | .filter(d => /\.less$/.test(d)).map(d => `${pre}"${dir}${d}";`).join('\n') 43 | }), Object.assign({ 44 | javascriptEnabled: true, 45 | paths: [path.dirname(pathname)], 46 | sourceMap: { 47 | sourceMapURL: outputMapPath.split('/').pop(), 48 | outputSourceFiles: true 49 | }, 50 | compress: !!build 51 | }, useLess), function (err, output) { 52 | if (err) { 53 | reject(err) 54 | } else if (output) { 55 | output.imports.map(dep => { 56 | const d = dep.match(pathREG) || [] 57 | const p = pathname.match(pathREG) || [] 58 | _.set(imports, [d.join('/'), p.join('/')], 1) 59 | conf.__ignores__.add(d.join('/')) 60 | }) 61 | let data = output.css + '' 62 | if (output.map) { 63 | store._set(outputMapPath, output.map) 64 | } 65 | resolve({ data, outputPath, end: true }) 66 | } 67 | }) 68 | }).catch(err => console.trace(pathname, err)) 69 | } 70 | }, 71 | buildWatcher (pathname, type, build) { 72 | const importsMap = imports[pathname] 73 | if (type === 'change' && importsMap) { 74 | Object.keys(importsMap).map(dep => { 75 | build(dep) 76 | }) 77 | } 78 | }, 79 | outputFilter (pathname, data) { 80 | return !/\.less$/.test(pathname) 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /lib/middleware/livereload.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const mime = require('mime') 3 | 4 | /** 5 | * @type {import('../../index').MiddlewareCreater} 6 | */ 7 | module.exports = (conf) => { 8 | const { 9 | renderHeaders = h => h, 10 | livereload 11 | } = conf 12 | 13 | /** 14 | * @type {import('../../index').LiveReloadConfig} 15 | */ 16 | let options 17 | if (!livereload) { 18 | return 19 | } else { 20 | options = Object.assign({ 21 | prefix: 'server-sent-bit', 22 | publicPath: '', 23 | heartBeatTimeout: 100000 24 | }, livereload === true ? {} : livereload) 25 | } 26 | const SERVER_SENT_SCRIPT = ``.replace(/[\r\n\s]+/g, ' ') 54 | /** 55 | * @type {Set} 56 | */ 57 | let responseSet = new Set([]) 58 | let updateTime = Date.now() 59 | const serverSent = (req, resp) => { 60 | resp.writeHead(200, renderHeaders({ 61 | 'Content-Type': 'text/event-stream', 62 | 'Cache-Control': 'no-cache', 63 | 'Connection': 'keep-alive' 64 | }, req)) 65 | responseSet.add(resp) 66 | send(updateTime) 67 | req.connection.addListener('close', () => { 68 | responseSet.delete(resp) 69 | resp.end() 70 | }, false) 71 | } 72 | 73 | let heartBeatTimeout 74 | const send = function send (time = updateTime) { 75 | updateTime = time 76 | for (let res of responseSet) { 77 | res.write(`data:${updateTime}\n\n`) 78 | } 79 | clearTimeout(heartBeatTimeout) 80 | heartBeatTimeout = setTimeout(function () { 81 | // keep SSE-connection by sending message per 100s 82 | send(updateTime) 83 | }, options.heartBeatTimeout) 84 | } 85 | 86 | let store 87 | return { 88 | onRoute: (pathname, req, resp, memory) => { 89 | if (pathname === options.prefix) { 90 | serverSent(req, resp) 91 | return false 92 | } 93 | }, 94 | buildWatcher: (pathname, eventType, build, _store) => { 95 | if (!store) { 96 | store = _store 97 | let last_building = false 98 | store.onBuildingChange(function (building) { 99 | if (last_building && !building) { 100 | send(Date.now()) 101 | } 102 | last_building = building 103 | }) 104 | } 105 | }, 106 | onText: (pathname, data, req, resp, memory) => { 107 | const type = mime.getType(pathname) 108 | if ( 109 | type && /html/.test(type) && 110 | !/XMLHttpRequest/i.test(req.headers['x-requested-with'] + '') 111 | ) { 112 | return (data && data.toString()) + SERVER_SENT_SCRIPT 113 | } else { 114 | return data 115 | } 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /lib/server/index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const PORT = require('../util/PORT') 3 | const IP = require('../util/IP') 4 | const { renderConf, setConfig, getConfig } = require('../conf') 5 | const os = require('os') 6 | const H = { 7 | http: require('http'), 8 | https: require('https') 9 | } 10 | const { spawn } = require('child_process') 11 | /** 12 | * @param {import("http") | import("https")} h 13 | * @param {string} base 14 | * @returns {(url: string) => Promise} 15 | */ 16 | const renderPromise = (h, base) => (url) => new Promise((resolve, reject) => h.get(base + url, resolve).on('error', reject)) 17 | 18 | /** 19 | * @param {import('../../').F2EConfig} conf 20 | * @param {import('http').Server} server 21 | */ 22 | const doInit = (conf, server) => { 23 | const {port = 80, host = IP, open = false, init_urls = [''], onServerCreate} = conf 24 | const protocal = port === 443 ? 'https' : 'http' 25 | const base = protocal + '://' + host + ':' + port 26 | const toPromise = renderPromise(H[protocal], base) 27 | console.log('waiting for start ...') 28 | Promise.all(init_urls.map(toPromise)).then(function () { 29 | console.log(`server start on ${base}`) 30 | open && spawn(os.type().match(/Windows/) ? 'explorer' : 'open', [base]) 31 | onServerCreate && onServerCreate(server, conf) 32 | }).catch(err => console.log(err)) 33 | } 34 | 35 | /** 36 | * @param {import('../../').F2EConfig} conf 37 | * @returns 38 | */ 39 | const createServer = (conf) => { 40 | const { port, ssl_options = {} } = conf 41 | const listener = renderListener(port) 42 | if (port === 443) { 43 | return H.https.createServer(ssl_options, listener).listen(port) 44 | } 45 | return H.http.createServer(listener).listen(port) 46 | } 47 | 48 | // 根据host和port寻找服务 49 | const renderListener = (port = 80) => { 50 | const map_app = new Map() 51 | const initing = new Set() 52 | /** 53 | * @param {import('http').IncomingMessage} req 54 | * @param {import('http').ServerResponse} resp 55 | * @returns 56 | */ 57 | const listener = async function (req, resp) { 58 | const [hostname] = (req.headers.host + '').split(':') 59 | const host1 = `${hostname}:${port}` 60 | const host2 = `:${port}` 61 | const _app = map_app.get(host1) || map_app.get(host2) 62 | if (_app) { 63 | _app(req, resp) 64 | return 65 | } 66 | const conf = getConfig(host1) || getConfig(host2) 67 | const { handleError, handleSuccess } = require('../util/resp')(conf || {}) 68 | if (!conf) { 69 | handleError(resp, Error('host not found!'), req) 70 | return 71 | } 72 | if (!conf.app || typeof conf.app === 'string') { 73 | handleError(resp, Error('wrong config of app!'), req) 74 | return 75 | } 76 | if (initing.has(host1) || initing.has(host2)) { 77 | handleSuccess(req, resp, 'index.html', conf.page_init) 78 | return 79 | } 80 | initing.add(host1) 81 | initing.add(host2) 82 | const app = await conf.app(conf) 83 | initing.delete(host1) 84 | initing.delete(host2) 85 | map_app.set(`${conf.host || ''}:${conf.port}`, app) 86 | app(req, resp) 87 | } 88 | return listener 89 | } 90 | 91 | /** 92 | * @type {Map} 93 | */ 94 | const map_server = new Map() 95 | 96 | /** 97 | * 入口函数 处理配置项 98 | * @param {import('../../index').F2EConfig} _conf 99 | */ 100 | const entry = async (_conf) => { 101 | const conf = renderConf(_conf) 102 | if (conf.host) { 103 | conf.port = conf.port || 80 104 | setConfig(`${conf.host}:${conf.port}`, conf) 105 | // 如果端口已经开启服务,仅注册config 106 | const server = map_server.get(conf.port) 107 | if (server) { 108 | doInit(conf, server) 109 | return server 110 | } 111 | } else { 112 | setConfig(`:${conf.port}`, conf) 113 | } 114 | if (!conf.port) { 115 | conf.port = await PORT() 116 | setConfig(`:${conf.port}`, conf) 117 | } 118 | const server = createServer(conf) 119 | map_server.set(conf.port, server) 120 | doInit(conf, server) 121 | return server 122 | } 123 | 124 | module.exports = entry 125 | -------------------------------------------------------------------------------- /lib/middleware/include.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const path = require('path') 3 | const fs = require('fs') 4 | const { URLSearchParams } = require('url') 5 | const _ = require('lodash') 6 | const { pathname_fixer } = require('../util/misc') 7 | 8 | /** 9 | * @type {import('../../index').MiddlewareCreater} 10 | */ 11 | module.exports = conf => { 12 | const { 13 | include = [ 14 | /\$include\[["'\s]*([^"'\s]+)["'\s]*\](?:\[["'\s]*([^"'\s]+)["'\s]*\])?/ 15 | // /@import\s*["']([^,"';]+),?([^,"';]*)["'];?/g 16 | ], 17 | belong = /\$belong\[["'\s]*([^"'\s]+)["'\s]*\]/, 18 | placeholder = '$[placeholder]', 19 | build, 20 | root 21 | } = conf 22 | 23 | /** 24 | * @param {string} urlPath 25 | * @param {string} minFilePath 26 | * @param {import('memory-tree').MemoryTree.Store} store 27 | * @returns {string} 28 | */ 29 | const includeFile = (urlPath, minFilePath, store) => { 30 | const [pathname, query] = urlPath.split('?') 31 | const param = new URLSearchParams(query) 32 | let data = store._get(pathname) 33 | let filePath = path.join(root, pathname) 34 | if (typeof data === 'undefined') { 35 | if (minFilePath) { 36 | minFilePath = path.join(root, minFilePath) 37 | } if (/[.-]min\.(\w+)$/.test(filePath)) { 38 | minFilePath = filePath 39 | } else { 40 | minFilePath = filePath.replace(/(\w+)$/, 'min.$1') 41 | } 42 | if (build && fs.existsSync(minFilePath)) { 43 | return fs.readFileSync(minFilePath).toString() 44 | } else { 45 | data = fs.readFileSync(filePath).toString().replace(/\$\{(\w+)\}/g, function (all, name) { 46 | return param[name] ? param[name].toString() : all 47 | }) 48 | } 49 | } 50 | return data && data.toString() 51 | } 52 | 53 | if (!include) { 54 | return 55 | } 56 | 57 | let imports = {} 58 | return { 59 | onSet (pathname, data, store) { 60 | if (_.isPlainObject(data)) { 61 | return data 62 | } 63 | try { 64 | if (conf.isText(pathname)) { 65 | const pathnameDir = pathname.replace(/[^\\/]+$/, '') 66 | /** @type {any} */ 67 | let belongStr = '' 68 | let str = data ? data.toString() : '' 69 | let h = belong.exec(str) 70 | 71 | if (h) { 72 | let belongPath = /^[\\/]/.test(h[1]) ? h[1] : path.join(pathnameDir, h[1]) 73 | belongStr = store._get(belongPath) || fs.readFileSync(path.join(root, belongPath)).toString() 74 | str = str.replace(h[0], '') 75 | str = belongStr.toString().replace(placeholder, str) 76 | const d = pathname_fixer(belongPath) 77 | _.set(imports, [d, pathnameDir], 1) 78 | conf.__ignores__.add(d) 79 | } 80 | for (let i = 0; i < include.length; i++) { 81 | const _include = include[i] 82 | while (_include.test(str)) { 83 | str = str.replace(_include, (al, filename, minFilePath, index) => { 84 | let includePath = /^[\\/]/.test(filename) ? filename : path.join(pathnameDir, filename) 85 | const d = pathname_fixer(includePath) 86 | _.set(imports, [d, pathnameDir], 1) 87 | conf.__ignores__ && conf.__ignores__.add(d) 88 | if (typeof minFilePath !== 'string') { 89 | minFilePath = '' 90 | } else { 91 | minFilePath = /^[\\/]/.test(minFilePath) ? minFilePath : path.join(pathnameDir, minFilePath) 92 | } 93 | return includeFile(d, minFilePath, store) 94 | }) 95 | } 96 | } 97 | return str 98 | } 99 | } catch (e) { 100 | console.log(e) 101 | } 102 | }, 103 | buildWatcher (pathname, type, build) { 104 | const importsMap = imports[pathname] 105 | if (type === 'change' && importsMap) { 106 | Object.keys(importsMap).map(dep => { 107 | build(dep) 108 | }) 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /pages/dir.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%=pathname%>/ 8 | 30 | 31 | 32 |
    33 |
  • ..
  • 34 | <%=Object.keys(store).sort().map(k => `
  • ${k}
  • `).join('\n')%> 35 |
36 | 37 | <% if (conf.authorization) { %> 38 |
本页面支持拖拽上传文件(仅支持单文件,不支持文件夹) 39 |   也可点击上传 40 |
41 |
42 | 新建文件夹 43 |
44 | 132 | <% } %> 133 | 134 | 135 | -------------------------------------------------------------------------------- /lib/apps/memory-tree/index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { URL } = require('url') 3 | const _ = require('lodash') 4 | const mime = require('mime') 5 | const MemoryTree = require('memory-tree').default 6 | const Middleware = require('../../middleware/index') 7 | const Resp = require('../../util/resp') 8 | const { decode, pathname_fixer, queryparam, isText } = require('../../util/misc') 9 | 10 | /** 11 | * @param {import("../../..").F2EConfig} conf 12 | */ 13 | module.exports = async (conf) => { 14 | const middleware = Middleware(conf) 15 | const RespUtil = Resp(conf) 16 | 17 | /** 18 | * @param { import('http').IncomingMessage } req 19 | * @param { import('http').ServerResponse } resp 20 | * @param { string } pathname 21 | */ 22 | const exec = async (req, resp, pathname) => { 23 | const routeResult = await middleware.onRoute(pathname, req, resp, memory.store) 24 | if (routeResult === false) { 25 | return 26 | } else if (typeof routeResult === 'string') { 27 | pathname = routeResult || pathname 28 | } 29 | pathname = pathname_fixer(pathname) 30 | const { 31 | handleSuccess, 32 | handleNotFound, 33 | handleDirectory 34 | } = RespUtil 35 | let data = await memory.store.load(pathname) 36 | if (_.isPlainObject(data)) { 37 | data = handleDirectory(req, resp, pathname, data) 38 | if (data === false) { return } 39 | data = await middleware.onText('html', data, req, resp, memory.store) 40 | if (data === false) { return } 41 | handleSuccess(req, resp, 'html', data) 42 | } else if (typeof data !== 'undefined') { 43 | if (isText(pathname)) { 44 | data = await middleware.onText(pathname, data, req, resp, memory.store) 45 | if (data === false) { 46 | return 47 | } 48 | } 49 | handleSuccess(req, resp, pathname, data) 50 | } else { 51 | handleNotFound(req, resp, pathname) 52 | } 53 | } 54 | 55 | /** 56 | * @param {import('../../../').F2EConfig} conf 57 | * @returns {import('memory-tree').MemoryTree.Options} 58 | */ 59 | const memoryConfig = (conf) => ({ 60 | root: conf.root || '', 61 | watch: conf.watch || !!conf.livereload, 62 | dest: conf.output, 63 | onSet: middleware.onSet, 64 | onGet: middleware.onGet, 65 | buildWatcher: middleware.buildWatcher, 66 | watchFilter: middleware.watchFilter, 67 | buildFilter: middleware.buildFilter, 68 | outputFilter: middleware.outputFilter 69 | }) 70 | const memory = MemoryTree(memoryConfig(conf)) 71 | await memory.input('') 72 | conf.onContextReady && conf.onContextReady({ 73 | middleware, memory 74 | }) 75 | /** 76 | * @param {import('../../../').RequestWith} req 77 | * @param {import('http').ServerResponse} resp 78 | */ 79 | const app = async (req, resp) => { 80 | const location = new URL('http://127.0.0.1' + req.url) 81 | let pathname = pathname_fixer(decode(location.pathname)) 82 | req.data = queryparam(location.searchParams) 83 | let pathnameTemp = await middleware.beforeRoute(pathname, req, resp, conf) 84 | if (pathnameTemp === false) { 85 | return 86 | } else if (typeof pathnameTemp === 'string') { 87 | pathname = pathnameTemp 88 | } 89 | if (req.method && req.method.toUpperCase() === 'GET') { 90 | exec(req, resp, pathname) 91 | } else { 92 | let chunks = [] 93 | req.on('data', chunk => { 94 | chunks.push(chunk) 95 | }).on('end', () => { 96 | const raw = req.rawBody = Buffer.concat(chunks) 97 | req.body = raw.toString('utf-8') 98 | const defaultType = 'application/x-www-form-urlencoded' 99 | const type = (req.headers && req.headers['content-type']) || defaultType 100 | const useParser = (conf.shouUseBodyParser || function (pathname, max_size) { 101 | return max_size < 100 * 4096 102 | })(pathname, raw.length) 103 | req.post = {} 104 | if (useParser) { 105 | try { 106 | switch (type) { 107 | case mime.getType('.json'): 108 | req.body = JSON.parse(req.body || '{}') 109 | break 110 | default: 111 | const loc = new URL('http://127.0.0.1') 112 | loc.search = decode(req.body || '') 113 | req.post = queryparam(loc.searchParams) || {} 114 | break 115 | } 116 | } catch (e) { 117 | console.error(e) 118 | } 119 | } 120 | exec(req, resp, pathname) 121 | }) 122 | } 123 | } 124 | 125 | return app 126 | } 127 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## v2.20.13 4 | - Typescript: 优化 `onRoute` 参数定义 5 | ## v2.20.12 6 | - cli: 修改cli命令提供一键生成模板 7 | ## v2.20.11 8 | - 优化: 目录页面上传问题修复等 9 | ## v2.20.10 10 | - 严重BUG: shouUseBodyParser 默认错误 11 | ## v2.20.9 12 | - BUG: 修改识别 port 方案 13 | ## v2.20.8 14 | - 修改: 准备页面 page_init 支持配置纯静态HTML文本 15 | - Typescript: 优化 RequestWith 参数描述 16 | ## v2.20.6 17 | - 功能: 新增 `shouldUseCompressor` 过滤是否需要进行gzip等类型压缩的资源 18 | - 修改: 内置请求扩展属性格式 `rawBody` 修改为 `Buffer` 19 | - 修改: `max_body_parse_size` 修改为 `shouldUseBodyparser` 20 | ## v2.20.5 21 | - 兼容修复: 代码方式配置仍然支持获取默认配置文件配置参数,优先级低于代码级别 22 | ## v2.20.3 23 | - **重大修改**: 为了支持完整的代码方式配置,重构了config初始化和识别方式 24 | ## v2.20.2 25 | - 配置修改: 修改配置文件检索方式,使用代码方式启动不再依赖配置文件,需要自己在入口完成所有配置 26 | - 重构: 根据tslint错误重构压缩器和输出模块代码 27 | - 修改: handleError支持参数request 28 | - 修改: 移除 @types/node 开发依赖 29 | - 修改: 代码方式启动后默认 livereload=false 30 | - 修改: .d.ts 添加 open 参数,表示服务启动后打开浏览器, 等于命令行的 -O 31 | ## v2.19.3 32 | - BUG & Feutre: 修改 `rename` 和 hash相关,所有资源均生成hash 33 | ## v2.19.2 34 | - TypeScript: 修改 .d.ts 描述 35 | ## v2.19.1 36 | - 功能: 删除keys目录,添加 `ssl_options` 支持手工配置ssl 37 | - 配置: `ignore_events` 支持过滤掉watch文件修改事件 38 | ## v2.18.8 39 | - Del: 删除创建APP的命令 40 | - Del: 修改默认conf取消所有依赖 41 | ## v2.18.6 42 | - TypeScript: 修改 .d.ts 描述 43 | - esline: eslint 更新 44 | ## v2.18.5 45 | - 功能: `rename` 支持修改所有资源名称 46 | - 功能: `SetResult.end` 判断是否截断 `onSet` 操作链 47 | ## v2.18.3 48 | - BUG: `namehash` 测试修改 49 | - 修改: **onSet过程中如果修改输出路径直接跳出并返回结果** 50 | ## v2.18.0 51 | - Del: 移除`bundles`相关内容 52 | - 功能: 原来的`buildFilter`,拆分新增的`watchFilter`,原来`buildFilter`仅用于拦截编译 53 | - 功能: 新增 `namehash` 功能,支持资源路径替换 54 | 55 | ## v2.17.5 56 | - Deps: 锁定template中间件版本,并修改依赖 57 | ## v2.17.4 58 | - TypeScript: onServerCreate 参数修复 http.Server 59 | ## v2.17.3 60 | - TypeScript: 修改 .d.ts 描述 61 | ## v2.17.2 62 | - Update: 更新依赖包,mime更新到3.0.0 63 | ## v2.17.1 64 | - BUG: 压缩模块处理了非文本文件 65 | ## v2.17.0 66 | - isText 全局整理 67 | - uglify-es -> terser 68 | - $include 不再支持压缩 69 | ## v2.16.17 70 | - 功能: 新增配置 `compressors` 扩展gzip压缩方案 71 | ## v2.16.16 72 | - BUG: `livereload` hide时候sse的undefined引用属性异常 73 | ## v2.16.15 74 | - 新增参数`onContextReady`: 获取环境内主要对象 75 | ## v2.16.13 76 | - memory-tree 更新 77 | ## v2.16.12 78 | - types: watch参数暴露 79 | ## v2.16.11 80 | - 编译依赖报错退出修改为 `error(exit 1)` 81 | ## v2.16.10 82 | - 根据eslint格式修改优化 83 | ## v2.16.9 84 | - livereload 优化: 根据文档 visibilitychange 事件修改 serversent 连接状态 85 | ## v2.16.8 86 | - BUG:v2.16.7 版本 livereload组件修改 导致全局onText渲染错误 87 | ## v2.16.7 88 | - 更新memory-tree版本到 0.6.18 89 | - 修改livereload参数支持配置修改 `prefix` 和 `heartBeatTimeout` 90 | ## v2.16.6 91 | - 更新less版本依赖到4.x 92 | ## v2.16.4 93 | - middleware `try_files` 支持多目录的index 94 | ## v2.16.3 95 | - middleware `try_files` 参数修改支持`location` 96 | ## v2.16.2 97 | - middleware `try_files` 参数修改支持`replacer` 98 | ## v2.16.1 99 | - middleware 所有事件均修改为支持Promise(有遗漏) 100 | ## v2.16.0 101 | - ts 修改 102 | - middleware 所有事件均修改为支持Promise 103 | 104 | ## v2.15.1 105 | - renderHeader修改,区分文本类型再新增编码 106 | 107 | ## v2.15.0 108 | - 配置文件错误时直接退出系统 109 | - 新增`try_files`配置内置中间件,支持类似nginx的try_files配置 110 | 111 | ## v2.14.14 112 | - types 修改 113 | ## v2.14.13 114 | - BUG修复 mime判断 text 错写为 txt 115 | ## v2.14.12 116 | - `req.data` 和 `req.post` 参数使用 `new URL().searchParams` 获取并转为 `NodeJS.Dict` 117 | 118 | ## v2.14.10 119 | - 静态服务路由支持过滤 `buildFilter` 120 | - 静态服务路由修改 `onRoute` 为 `beforeRoute` 121 | 122 | ## v2.14.9 123 | - 静态服务路由解析字符问题修改 124 | 125 | ## v2.14.8 126 | - 静态服务APP恢复 127 | - `url.parse` 修改为 `new URL` 128 | 129 | ## v2.14.6 130 | - 默认目录配置修改 131 | 132 | ## v2.14.5 133 | - 修改 files 配置,防止提交不需要的文件到npm仓库 134 | 135 | ## v2.14.4 136 | - 修改参数 include 将 2.14.3 默认新增的 @import 去掉 【影响less编译】 137 | 138 | ## v2.14.3 139 | - 修改参数 include 为RegExp[] 可以支持多组正则替换, 默认支持css @import 140 | 141 | ## v2.14.2 142 | - BUG修改 init_urls 触发时机修改 143 | 144 | ## v2.14.1 145 | - 升级依赖 memmory-tree@0.6.15 146 | 147 | ## v2.14.0 148 | - 移除babel中间件以及 babel-core 依赖所有相关 149 | - 所有压缩相关依赖包移至 devDependencies 150 | 151 | ## v2.13.7 152 | - beforeRoute 开始生效 153 | 154 | ## v2.13.5 155 | - 添加参数 `max_body_parse_size` 请求body转化为UTF8字符串长度小于100K时候进行parse 156 | 157 | ## v2.13.2 158 | - renderHeaders 支持添加统一响应头渲染参数 159 | - port 自动寻找功能删除, 项目必须显示的配置port 160 | ## v2.13.0 161 | - less 中间件支持map输出, 支持过滤 import 导入的源文件编译和输出 162 | - include 模块支持过滤源文件编译和输出 163 | - 增加 log 输出每个资源的构建时间方便调试 参数为 `__withlog__` 164 | ## v2.12.13 165 | - build 设置cofig文件BUG修复 166 | 167 | ## v2.12.12 168 | - update包依赖 169 | - 修改 `index.d.ts` 描述文件 170 | - 内置中间件添加ts-check 171 | 172 | ## v2.12.4 173 | - update包依赖 174 | - 修改 `include` 中间件的js代码压缩 175 | - 修改 `index.d.ts` 描述文件 176 | 177 | ## v2.12.2 178 | - 支持配置 `init_urls?: string[]` 当服务器启动时初始化这些url 179 | - 目录自动更新BUG修改 180 | - 支持配置 `authorization?: string` 181 | - 提供验证账户密码 格式为 `${name}:${password}` 182 | - 配置后支持目录页面下新增删除文件(夹) 183 | 184 | ## v2.12.0 185 | - 支持 `page` 相关配置 186 | - page_404 187 | - page_50x 188 | - page_dir 189 | - 移除 `onDirectory` 配置 190 | 191 | ## v2.11.0 192 | - 支持指定配置文件 命令 `f2e start -c [.f2econfig.js文件相对路径]` 193 | - build模式对js、css后缀文件 不生效严重BUG修复 194 | - 将服务初始化时机从 *首次接收到访问* 修改为 *启动时打开浏览器之前* 195 | 196 | ## v2.10.0 197 | - 支持 `onServerCreate` 事件,可以在启动时接受 `server` 参数用于创建 Websocket等服务 198 | - 修改支持 `no_host: true` 配置,端口服务单例化 (默认同一端口支持根据不同的host访问不同的配置服务) 199 | - 添加启动参数判断, 当 `process.argv.includes('start')` 时,启动后打开浏览器 200 | 201 | ## v2.9.16 202 | - `querystring.parse` 直接解析search参数 `+` 字符处理 203 | - babel 中间件默认sourceMap配置项修改为false 204 | 205 | ## v2.9.4 206 | - 支持 range 请求, 默认大小 1024 * 1024,支持配置参数 `range_size` 207 | - 增加 ts 描述文档 208 | -------------------------------------------------------------------------------- /lib/util/resp.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const mime = require('mime') 3 | const fs = require('fs') 4 | const path = require('path') 5 | const ETag = require('etag') 6 | const { getCompressor } = require('./compressor') 7 | const _ = require('lodash') 8 | // @ts-ignore 9 | const pkg = require('../../package.json') 10 | const misc = require('./misc') 11 | 12 | const version = `${pkg.name} ${pkg.version}` 13 | /** 14 | * @param {import('../../index').F2EConfig} conf 15 | */ 16 | const handleRange = (range, { req, resp, pathname, data, newTag }, conf) => { 17 | const renderHeaders = conf.renderHeaders || (h => h) 18 | const size = conf.range_size || 1024 * 1024 19 | let [start, end] = range.replace(/[^\-\d]+/g, '').split('-') 20 | start = start | 0 21 | end = (end | 0) || (start + size) 22 | const d = data.slice(start, end) 23 | end = Math.min(end, start + d.length) 24 | resp.writeHead(206, renderHeaders({ 25 | 'Content-Type': mime.getType(pathname) || 'text/html', 26 | 'X-Powered-By': version, 27 | 'Accept-Ranges': 'bytes', 28 | 'Content-Length': d.length, 29 | 'Content-Range': `bytes ${start}-${end - 1}/${data.length}`, 30 | 'ETag': newTag 31 | }, req)) 32 | resp.end(d) 33 | } 34 | 35 | /** 36 | * @param {Required} conf 37 | */ 38 | module.exports = conf => { 39 | const { root, page_404, page_50x, page_dir, isText = misc.isText } = conf || {} 40 | const renderHeaders = conf.renderHeaders || (h => h) 41 | const template_404 = typeof page_404 === 'string' 42 | ? (compile => (req, resp, param = {}) => compile(param))(_.template(fs.readFileSync(page_404).toString())) 43 | : page_404 44 | const template_50x = typeof page_50x === 'string' 45 | ? (compile => (req, resp, param = {}) => compile(param))(_.template(fs.readFileSync(page_50x).toString())) 46 | : page_50x 47 | const template_dir = typeof page_dir === 'string' 48 | ? (compile => (req, resp, param = {}) => compile(param))(_.template(fs.readFileSync(page_dir).toString())) 49 | : page_dir 50 | const authorization = conf.authorization && Buffer.from(conf.authorization).toString('base64') 51 | 52 | /** 53 | * @param {import('http').ServerResponse} resp 54 | * @param {Error} error 55 | * @param {import('http').IncomingMessage} req 56 | */ 57 | const handleError = (resp, error, req) => { 58 | resp.writeHead(500, renderHeaders({ 59 | 'Content-Type': 'text/html; charset=utf-8', 60 | 'X-Powered-By': version 61 | })) 62 | resp.end(template_50x ? template_50x(req, resp, { error }) : (error ? error.toString() : '500')) 63 | return resp 64 | } 65 | 66 | /** 67 | * @param {import('http').IncomingMessage} req 68 | * @param {import('http').ServerResponse} resp 69 | * @param {string} pathname 70 | * @param {Buffer|string|import('fs').Stats} data 71 | */ 72 | const handleSuccess = (req, resp, pathname, data) => { 73 | const tag = req.headers['if-none-match'] 74 | const newTag = data && ETag(data) 75 | const txt = isText(pathname) 76 | const compressionAlgorithm = txt ? getCompressor(req, conf) : null 77 | let header = renderHeaders({ 78 | 'Content-Type': mime.getType(pathname) + (txt ? '; charset=utf-8' : ''), 79 | 'Content-Encoding': compressionAlgorithm ? compressionAlgorithm.contentEncoding : 'utf-8', 80 | 'X-Powered-By': version 81 | }, req) 82 | if (tag && data && tag === newTag) { 83 | resp.writeHead(304, header) 84 | resp.end() 85 | } else if (req.headers['range'] && data instanceof Buffer) { 86 | handleRange(req.headers['range'], { req, resp, pathname, data, newTag }, conf) 87 | } else { 88 | newTag && (header['ETag'] = newTag) 89 | resp.writeHead(200, header) 90 | if (data instanceof fs.Stats) { 91 | let stream = fs.createReadStream(path.join(root, pathname)) 92 | if (compressionAlgorithm) { 93 | stream.pipe(compressionAlgorithm.createStream()).pipe(resp) 94 | } else { 95 | stream.pipe(resp) 96 | } 97 | } else { 98 | resp.end(compressionAlgorithm ? compressionAlgorithm.compressSync(data.toString()) : data) 99 | } 100 | } 101 | } 102 | 103 | /** 104 | * @param {import('http').IncomingMessage} req 105 | * @param {import('http').ServerResponse} resp 106 | * @param {string} pathname 107 | */ 108 | const handleNotFound = (req, resp, pathname) => { 109 | resp.writeHead(404, renderHeaders({ 110 | 'Content-Type': 'text/html; charset=utf-8', 111 | 'X-Powered-By': version 112 | }, req)) 113 | resp.end(template_404(req, resp, { pathname })) 114 | return resp 115 | } 116 | 117 | /** 118 | * @param {import('http').IncomingMessage} req 119 | * @param {import('http').ServerResponse} resp 120 | * @param {string} pathname 121 | * @param {object} store 122 | * @returns {string | false} 123 | */ 124 | const handleDirectory = (req, resp, pathname, store) => { 125 | const method = req.method && req.method.toUpperCase() 126 | if (method === 'GET') { 127 | return template_dir(req, resp, { pathname: misc.pathname_fixer(pathname), dirname: misc.pathname_dirname(pathname), store, conf }) 128 | } 129 | const { headers = {} } = req 130 | if (!authorization || headers.authorization !== 'Basic ' + authorization) { 131 | resp.statusCode = 401 132 | resp.setHeader('WWW-Authenticate', 'Basic realm="example"') 133 | resp.end('Access denied') 134 | return false 135 | } 136 | let { file = '' } = req['data'] 137 | file = file.replace(/\/+/g, '') 138 | if (!file) { 139 | resp.statusCode = 403 140 | resp.end('缺少参数') 141 | return false 142 | } 143 | const file_path = path.join(root, pathname + '/' + file) 144 | try { 145 | switch (method) { 146 | case 'DELETE': 147 | const stat = fs.statSync(file_path) 148 | stat.isFile() && fs.unlinkSync(file_path) 149 | stat.isDirectory() && fs.rmdirSync(file_path) 150 | delete store[file] 151 | break 152 | case 'PUT': 153 | fs.writeFileSync(file_path, req['rawBody']) 154 | break 155 | case 'POST': 156 | fs.mkdirSync(file_path) 157 | store[file] = {} 158 | break 159 | default: 160 | } 161 | } catch (e) { 162 | handleError(resp, e, req) 163 | } 164 | handleSuccess(req, resp, '.json', '{"success": true}') 165 | return false 166 | } 167 | return { 168 | handleError, 169 | handleSuccess, 170 | handleNotFound, 171 | handleDirectory 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /lib/middleware/index.js: -------------------------------------------------------------------------------- 1 | const Build = require('./build') 2 | const TryFiles = require('./try_files') 3 | const { isPlainObject } = require('lodash') 4 | var crypto = require('crypto') 5 | 6 | /** @type {import('../../index').MiddlewareCreater[]} */ 7 | let middlewares = [ 8 | require('./include'), 9 | require('./less'), 10 | require('./livereload'), 11 | require('./namehash') 12 | ] 13 | 14 | /** 15 | * 16 | * @param {import('../../index').F2EConfig} conf 17 | * 18 | * @returns {Required & { onGet?: any, onSet: any }>} 19 | */ 20 | const mod = (conf) => { 21 | let allConfig = { 22 | beforeRoute: [].concat(conf.beforeRoute || []), 23 | onRoute: [].concat(conf.onRoute || []), 24 | buildWatcher: [].concat(conf.buildWatcher || []), 25 | onSet: [].concat(conf.onSet || []), 26 | onGet: [].concat(conf.onGet || []), 27 | onText: [].concat(conf.onText || []), 28 | buildFilter: [].concat(conf.buildFilter || []), 29 | watchFilter: [].concat(conf.watchFilter || []), 30 | outputFilter: [].concat(conf.outputFilter || []) 31 | } 32 | const keys = Object.keys(allConfig) 33 | 34 | middlewares 35 | .concat(conf.middlewares || []) 36 | .concat(TryFiles) 37 | .map((Middleware) => { 38 | let middle 39 | /** @type {any} */ 40 | const { middleware } = Middleware 41 | if (typeof Middleware === 'function') { 42 | middle = Middleware(conf) 43 | } else if (middleware) { 44 | const middlewareName = `f2e-middle-${middleware}` 45 | try { 46 | middle = require(middlewareName)(conf, Middleware) 47 | } catch (e) { 48 | console.error(e) 49 | } 50 | } 51 | middle && keys.map(key => { 52 | if (middle[key]) { 53 | if (typeof middle.setBefore === 'number') { 54 | allConfig[key].splice(middle.setBefore, 0, middle[key]) 55 | } else { 56 | allConfig[key].push(middle[key]) 57 | } 58 | } 59 | }) 60 | }) 61 | 62 | // build 模块需要在最后 63 | const build = Build(conf) 64 | build && keys.map(key => { 65 | if (build[key]) { 66 | allConfig[key].push(build[key]) 67 | } 68 | }) 69 | 70 | const ignore_events = conf['ignore_events'] 71 | const ignores = new Set(ignore_events || ['add', 'addDir']) 72 | /** 有过改写路径的编译资源 */ 73 | const input_output_map = new Map() 74 | return { 75 | beforeRoute: async (pathname, req, resp, conf) => { 76 | for (let i = 0; i < allConfig.beforeRoute.length; i++) { 77 | // beforeRoute 返回 false 停止继续 78 | let res = await Promise.resolve(allConfig.beforeRoute[i](pathname, req, resp, conf)) 79 | if (typeof res !== 'undefined') { 80 | return res 81 | } 82 | } 83 | }, 84 | onRoute: async (pathname, req, resp, memory) => { 85 | for (let i = 0; i < allConfig.onRoute.length; i++) { 86 | // onRoute 返回 false 停止继续 87 | let res = await Promise.resolve(allConfig.onRoute[i](pathname, req, resp, memory)) 88 | if (typeof res !== 'undefined') { 89 | return res 90 | } 91 | } 92 | }, 93 | buildWatcher: (pathname, eventType, build, store) => { 94 | if (conf.__withlog__ && !ignores.has(eventType)) { 95 | console.log(`${eventType}: ${pathname}`) 96 | } 97 | allConfig.buildWatcher.map(item => item(pathname, eventType, build, store)) 98 | }, 99 | onSet: async (pathname, data, store) => { 100 | /** 101 | * @type {import('../../index').SetResult} 102 | */ 103 | let temp = { data, originPath: pathname, outputPath: pathname, end: false } 104 | if (conf.__ignores__.has(pathname)) { 105 | return temp 106 | } 107 | let t = 0 108 | if (conf.__withlog__) { 109 | t = Date.now() 110 | } 111 | for (let i = 0; i < allConfig.onSet.length; i++) { 112 | let res = await Promise.resolve(allConfig.onSet[i](temp.outputPath, temp.data, store)) 113 | if (isPlainObject(res)) { 114 | Object.assign(temp, res) 115 | } else if (res) { 116 | temp.data = res 117 | } 118 | if (temp.end) { 119 | break 120 | } 121 | } 122 | if (conf.__withlog__) { 123 | console.log(`compile: ${pathname} ${Date.now() - t}ms`) 124 | } 125 | if (temp.data && !isPlainObject(temp.data)) { 126 | try { 127 | const hash = crypto.createHash('md5').update(temp.data).digest('hex') 128 | input_output_map.set(temp.originPath, { 129 | output: conf.rename ? conf.rename(temp.outputPath) : temp.outputPath, 130 | hash 131 | }) 132 | } catch (e) { 133 | console.error(e) 134 | } 135 | } 136 | return temp 137 | }, 138 | onGet: async (pathname, data, store) => { 139 | let temp = data 140 | for (let i = 0; i < allConfig.onGet.length; i++) { 141 | let res = await Promise.resolve(allConfig.onGet[i](pathname, temp || data, store, input_output_map)) 142 | temp = res || temp 143 | } 144 | return temp 145 | }, 146 | onText: async (pathname, data, req, resp, memory) => { 147 | for (let i = 0; i < allConfig.onText.length; i++) { 148 | let allow = await Promise.resolve(allConfig.onText[i](pathname, data, req, resp, memory)) 149 | if (allow === false) { 150 | return false 151 | } else { 152 | data = allow || data 153 | } 154 | } 155 | return data 156 | }, 157 | watchFilter: (pathname) => { 158 | for (let i = 0; i < allConfig.watchFilter.length; i++) { 159 | let allow = allConfig.watchFilter[i](pathname) 160 | if (allow === false) { 161 | return false 162 | } 163 | } 164 | return true 165 | }, 166 | buildFilter: (pathname) => { 167 | for (let i = 0; i < allConfig.buildFilter.length; i++) { 168 | let allow = allConfig.buildFilter[i](pathname) 169 | if (allow === false) { 170 | return false 171 | } 172 | } 173 | return true 174 | }, 175 | outputFilter: (pathname, data) => { 176 | if (conf.__ignores__.has(pathname)) { 177 | return false 178 | } 179 | for (let i = 0; i < allConfig.outputFilter.length; i++) { 180 | let allow = allConfig.outputFilter[i](pathname, data) 181 | if (allow === false) { 182 | return false 183 | } 184 | } 185 | return true 186 | } 187 | } 188 | } 189 | 190 | module.exports = mod 191 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # f2e-server 2 | f2e-server 2 3 | 4 | ## Install 5 | `npm i -g f2e-server` 6 | 7 | ## Options 8 | - `f2e -h` 9 | - `f2e conf` 生成 .f2econfig.js 配置文件 [.f2econfig.js](.f2econfig.js) 的一个clone版本,需要自行修改 10 | - `f2e build` 构建到 output 目录 (需要在配置文件中配置 output 路径) 11 | - `f2e build -w true` 开启构建并监听文件变化输出结果 12 | - `f2e start` 启动开发服务器 13 | - `f2e start -h` 14 | - `f2e start` 从 2850 开始自增检测可以使用的PORT并启动 15 | - `f2e start -c .f2econfig.js` 指定配置文件 16 | - `f2e start -p 8080` 指定端口启动 17 | - `sudo f2e start -p 443` 开启HTTPS支持 18 | - `sudo f2e start -H mysite.local` 设置本地域名并从80端口启动 19 | - `sudo f2e start -H mysite.local -p 8080` 设置本地域名并从指定端口启动 20 | 21 | ## Config 22 | `f2e conf` 生成 [.f2econfig.js](.f2econfig.js) 配置文件 23 | 24 | ### 基本配置 25 | 26 | ``` javascript 27 | 28 | const path = require('path') 29 | const request = require('request') 30 | 31 | module.exports = { 32 | // host: 'f2e.local.cn', 33 | /** 34 | * 不启用 host 识别,只根据端口处理 35 | */ 36 | no_host: false, 37 | // port: 2850, 38 | /** 39 | * 是否开启自动刷新 40 | * @type {Boolean} 41 | */ 42 | livereload: true, 43 | /** 44 | * 使用 less 编译为css, 使用 less 配置 45 | * @type {Object} 46 | */ 47 | useLess: { 48 | compress: false 49 | }, 50 | /** 51 | * 支持babel编译 js/es/jsx, 支持 `.babelrc` 配置, 52 | * @type {Object} 53 | */ 54 | useBabel: { 55 | getModuleId: pathname => pathname.replace(/\\+/g, '/'), 56 | /** 57 | * 支持多组babel-option 配置通过 only 参数匹配,匹配到一个,则停止 58 | */ 59 | _rules: [ 60 | { 61 | only: ['number.js'], 62 | getModuleId: pathname => 'number', 63 | } 64 | ] 65 | }, 66 | /** 67 | * 是否支持 gzip 68 | * @type {Boolean} 69 | */ 70 | gzip: true, 71 | /** 72 | * Range 默认大小 73 | * @type {Number} 74 | */ 75 | range_size: 1024 * 1024, 76 | /** 77 | * 支持中间件列表, 默认添加的系统中间件后面, build之前 78 | * 79 | * ☆ 重要的 80 | * 1. 自定义中间件中所有事件 onXxx 也支持在外面定义, 在组件内更显条理, 而且也方便自己封装插件多处引入 81 | * 2. 系统中间件顺序 include(0) -> less(1) -> babel(2) ---> build(last) 82 | * 3. 顶层定义的事件顺序在系统中间件之前 83 | * @type {Array} 84 | */ 85 | middlewares: [ 86 | // marked 编译 87 | (conf) => { 88 | // conf 为当前配置 89 | return { 90 | /** 91 | * 92 | * @param {string} pathname 当前资源路径 93 | * @param {Request} req 原生request对象 94 | * @param {Response} resp 原生response对象 95 | * @param {Object} memory 所有目录对应内存结构, get/set等方法调用会被 onSet/onGet 等拦截 96 | */ 97 | onRoute (pathname, req, resp, memory) { 98 | // 搞一个代理试试 99 | if (pathname.match(/^es6/)) { 100 | request(pathname.replace('es6', 'http://es6.ruanyifeng.com')).pipe(resp) 101 | return false 102 | } 103 | }, 104 | /** 105 | * 106 | * @param {string} eventType 事件类型 change/add/etc. 107 | * @param {string} pathname 当前修改文件的路径 108 | * @param {boolean} build 是否开启了build配置, build模式下可能同时需要触发其他资源修改等 109 | */ 110 | buildWatcher (eventType, pathname, build) { 111 | console.log(new Date().toLocaleString(), eventType, pathname) 112 | }, 113 | /** 114 | * onSet 设置资源内容时候触发 115 | * @param {string} pathname 当前资源路径 116 | * @param {string/Buffer} data 上一个流结束时候的数据 117 | * @param {object} store 数据仓库 {_get, _set} 118 | * @return {string/Buffer} 将要设置的内容 119 | */ 120 | onSet(pathname, data, store) { 121 | if (pathname.match(/\.md$/)) { 122 | let res = require('marked')(data.toString()) 123 | // 在数据仓库中设置一个新的资源 .html 124 | store._set(pathname.replace(/\.md$/, '.html'), res) 125 | } 126 | }, 127 | /** 128 | * 跟onSet类似, 开发环境下面,每次请求都会执行, 缩短server启动时间可以把onSet的逻辑扔这里 129 | */ 130 | onGet(pathname, data, store) {}, 131 | /** 132 | * 不希望影响构建的操作, 仅在server中触发, 不希望影响构建的操作(例: 自动更新脚本插入) 133 | */ 134 | onText(pathname, data, req, resp, memory) {}, 135 | buildFilter(pathname, data) {}, 136 | outputFilter (pathname, data) { 137 | // .md 资源server环境可见, 但是不输出 138 | return !/\.md$/.test(pathname) 139 | } 140 | } 141 | }, 142 | // lodash 模板引擎 143 | () => { 144 | const _ = require('lodash') 145 | return { 146 | // 中间件置顶位置 include 之后 147 | setBefore: 1, 148 | onSet (pathname, data) { 149 | // data 目录下面的文本资源需要经过模板引擎编译 150 | if (pathname.match(/^test\/.*.html/)) { 151 | let str = data.toString() 152 | try { 153 | str = _.template(str)({__dirname, require}) 154 | } catch (e) { 155 | console.log(pathname, e) 156 | } 157 | return str 158 | } 159 | } 160 | } 161 | } 162 | ], 163 | /** 164 | * 只构建指定条件的资源 165 | * @param {string} pathname 资源路径名 166 | * @param {Buffer/string} data 资源内容 167 | * @return {Boolean} 168 | */ 169 | buildFilter: (pathname, data) => { 170 | // 路径过滤 171 | let nameFilter = !pathname || /lib|test|index|README/.test(pathname) 172 | // 资源大小过滤 173 | let sizeFilter = !data || data.toString().length < 1024 * 1024 174 | return nameFilter && sizeFilter 175 | }, 176 | /** 177 | * build 阶段是否使用 uglify/cleanCSS 进行 minify 操作 178 | * @param {string} pathname 资源路径名 179 | * @param {Buffer/string} data 资源内容 180 | * @return {Boolean} 181 | */ 182 | shouldUseMinify: (pathname, data) => { 183 | let ok = data.toString().length < 1024 * 1024 184 | !ok && console.log('shouldNotUseMinify: ' + pathname) 185 | return ok 186 | }, 187 | /** 188 | * app 默认时候 f2e 构建系统, 支持 'static' 纯静态服务器 189 | * 如果 app 自定义, 相当于只是使用 f2e 的端口开启和域名解析功能, 其他所有配置失效 190 | */ 191 | // app: 'static', 192 | /** 193 | * 资源数据目录, 未设置的时候 build 中间件不开启 194 | * @type {local-url} 195 | */ 196 | output: path.resolve(__dirname, '../output'), 197 | /** 198 | * after server create 199 | * you can render websocket server via this 200 | */ 201 | onServerCreate: (server) => { 202 | const { Server } = require('ws') 203 | const wss = new Server({server}); 204 | wss.on('connection', (socket) => { 205 | socket.send('init') 206 | }) 207 | } 208 | } 209 | 210 | ``` 211 | 212 | ### 中间件 213 | 参考 [f2e-middleware](https://github.com/shy2850/f2e-middleware) 214 | 215 | 1. lodash 模板引擎 216 | 2. markdown 编译 217 | 3. proxy 请求代理配置 218 | 4. dest 构建资源输出重命名 219 | 5. qrcode 简单二维码生成器 220 | 221 | ### app接入 222 | 支持接入 [Koa](http://koajs.com/) 以及 [express](https://expressjs.com/) 223 | 224 | ``` javascript 225 | const Koa = require('koa') 226 | const app = new Koa() 227 | 228 | app.use(ctx => { 229 | ctx.body = __dirname 230 | }) 231 | 232 | 233 | const express = require('express') 234 | const app1 = express() 235 | 236 | app1.get('/', function (req, res) { 237 | res.send(__dirname) 238 | }) 239 | 240 | app1.use(express.static('lib')) 241 | 242 | module.exports = { 243 | // app: app.callback(), 244 | // app: 'static', // 纯静态资源服务器 245 | app: app1 246 | } 247 | ``` 248 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import { IncomingMessage, ServerResponse, OutgoingHttpHeaders, Server } from "http" 2 | import type * as https from 'https' 3 | import { MemoryTree } from "memory-tree" 4 | import { CompressorType } from "./lib/util/compressor" 5 | type LessConfig = Less.Options 6 | 7 | declare function f2eserver(...conf: f2eserver.F2EConfig[]): void 8 | declare namespace f2eserver { 9 | export interface RequestWith extends IncomingMessage { 10 | /** 11 | * 请求search参数 12 | */ 13 | data: Record, 14 | /** 15 | * 原始请求内容 16 | */ 17 | rawBody?: Buffer, 18 | /** 19 | * POST请求为 application/json 类型时,转换后的参数 20 | * 如果不转JSON或者转换失败, 转为utf8编码的字符串 21 | */ 22 | body?: T extends object ? T : string, 23 | /** 24 | * POST请求为 application/x-www-form-urlencoded 类型时,转化后的参数 25 | * @description 不支持文件上传 26 | */ 27 | post?: Record, 28 | } 29 | export type SetResult = MemoryTree.DataBuffer | { 30 | data: MemoryTree.DataBuffer; 31 | /** 源文件路径 */ 32 | originPath?: string; 33 | /** 修改后路径 */ 34 | outputPath?: string; 35 | /** 结束操作链返回当前结果 */ 36 | end?: boolean; 37 | } 38 | export interface F2Events { 39 | /** 40 | * on request begin 41 | */ 42 | beforeRoute?: { 43 | (pathname: string, req: IncomingMessage, resp: ServerResponse, conf?: F2EConfig): string | false | void | Promise 44 | } 45 | /** 46 | * on request end 47 | */ 48 | onRoute?: { 49 | (pathname: string, req: RequestWith, resp: ServerResponse, store?: MemoryTree.Store): string | false | void | Promise 50 | } 51 | /** 52 | * on file change 53 | */ 54 | buildWatcher?: { 55 | (pathname: string, eventType: string, build: MemoryTree.Build, store: MemoryTree.Store): void 56 | } 57 | /** 58 | * on data save into memory 59 | */ 60 | onSet?: { 61 | (pathname: string, data: MemoryTree.DataBuffer, store: MemoryTree.Store): SetResult | Promise 62 | } 63 | /** 64 | * get data from memory 65 | */ 66 | onGet?: { 67 | (pathname: string, data: MemoryTree.DataBuffer, store: MemoryTree.Store, input_output_map: Map): MemoryTree.DataBuffer | Promise 71 | } 72 | /** 73 | * if text on request 74 | */ 75 | onText?: { 76 | (pathname: string, data: MemoryTree.DataBuffer, req: IncomingMessage, resp: ServerResponse, store: MemoryTree.Store): MemoryTree.DataBuffer | false | Promise 77 | } 78 | /** 79 | * whether to watch some path from disk 80 | */ 81 | watchFilter?: { 82 | (pathname: string): boolean 83 | } 84 | /** 85 | * whether to build some path from disk 86 | */ 87 | buildFilter?: { 88 | (pathname: string): boolean 89 | } 90 | /** 91 | * whether to persist data by outputPath 92 | */ 93 | outputFilter?: { 94 | (pathname: string, data: MemoryTree.DataBuffer): boolean 95 | } 96 | } 97 | export interface Middleware extends F2Events { 98 | /** 99 | * turn of middle to execute 100 | */ 101 | setBefore?: number 102 | } 103 | export interface MiddlewareCreater { 104 | (conf: Required, options?: any): Middleware | undefined | null 105 | } 106 | export interface MiddlewareRef { 107 | /** 108 | * middleware name 109 | * > if you middleware named `f2e-middle-markdown` then use 'markdown' here 110 | * > ## middleware should implements MiddlewareCreater 111 | */ 112 | middleware: string 113 | /** 114 | * turn of middle to execute 115 | */ 116 | setBefore?: number 117 | /** 118 | * support muti options for any middlewares 119 | */ 120 | [x: string]: any 121 | } 122 | 123 | export interface PageRender { 124 | (req: IncomingMessage, resp: ServerResponse, data: T): R 125 | } 126 | 127 | export type TryFilesItem = { 128 | test: RegExp, 129 | replacer?: string | { (m: string, ...args: any[]): string }, 130 | } & ( 131 | { index: string | { (pathname: string, req: IncomingMessage, resp: ServerResponse): string } } 132 | | { location: string | { (pathname: string, req: IncomingMessage, resp: ServerResponse): string } } 133 | ) 134 | 135 | export interface LiveReloadConfig { 136 | prefix?: string 137 | publicPath?: string 138 | heartBeatTimeout?: number 139 | } 140 | export interface F2EConfig extends F2Events { 141 | /** 项目根路径 */ 142 | root?: string 143 | /** 默认从2850开始找未使用的端口, 配置后不检测, 144 | * 当配置端口为443的时候自动转化为 https 服务并需要配置 ssl_options */ 145 | port?: number 146 | /** 是否打开浏览器, 依赖本地命令 open 打开 */ 147 | open?: boolean 148 | /** 149 | * ssl 配置 150 | * 如: { key: string, cert: string } 151 | * */ 152 | ssl_options?: https.ServerOptions 153 | /** 154 | * 指定host访问生效 155 | * 未指定时,只要是访问端口符合就可以,相当于nginx的 servername: _ 156 | */ 157 | host?: string 158 | /** 开启监听文件修改 */ 159 | watch?: boolean 160 | /** 161 | * 忽略文件更新事件 162 | * @default ['add', 'addDir'] 163 | */ 164 | ignore_events?: ("add" | "addDir" | "change" | "unlink" | "unlinkDir")[] 165 | /** 开启监听文件修改,并植入sse监测脚本 */ 166 | livereload?: boolean | LiveReloadConfig 167 | build?: boolean 168 | gzip?: boolean 169 | /** gzip 压缩扩展,支持更多压缩方式 */ 170 | compressors?: CompressorType[] 171 | /** 172 | * 运行时 是否对资源进行 gzip等压缩 173 | * 可以根据文件路径、文件大小给出结果 174 | * @default function (pathname, min_size) { return isText(pathname) && min_size > 4096 } 175 | * @param {string} pathname 资源路径名 176 | * @param {number} data 资源大小 177 | * @return {boolean} 178 | */ 179 | shouldUseCompressor?: (pathname: string, min_size: number) => boolean 180 | /** 181 | * build 阶段是否使用 terser/uglify/cleanCSS 进行 minify 操作 182 | * 可以根据文件路径或者文件内容、大小给出结果 183 | * @default function () { return true } 184 | * @param {string} pathname 资源路径名 185 | * @param {MemoryTree.DataBuffer} data 资源内容 186 | * @return {boolean} 187 | */ 188 | shouldUseMinify?: (pathname: string, data: MemoryTree.DataBuffer) => boolean 189 | /** 190 | * post是否进行请求参数封装 191 | * 1. 请求头JSON格式, 会执行JSON.parse并将结果赋值于 request.body 上, 192 | * 2. 请求头非JSON,会执行表单反序列化转化成对象将结果赋值于 request.post 上 193 | * 3. 不封装时,request.rawBody 为原始请求数据, request.body 为原始数据转 utf8 格式字符串 194 | * @default function (pathname, max_size) { return min_size < 100 * 4096 } 195 | * @param {string} pathname 请求路径 196 | * @param {number} max_size 请求内容大小 197 | * @returns 198 | */ 199 | shouUseBodyParser?: (pathname: string, max_size: number) => boolean 200 | /** 201 | * stream data output size per response 202 | */ 203 | range_size?: number 204 | useLess?: boolean | LessConfig 205 | /** 206 | * 启用babel时候是否 207 | */ 208 | sourceMap?: boolean 209 | 210 | /** 211 | * @default [/\$include\[["'\s]*([^"'\s]+)["'\s]*\](?:\[["'\s]*([^"'\s]+)["'\s]*\])?/g] 212 | */ 213 | include?: RegExp[] 214 | /** 215 | * @default /^@import["'\s]+(.+)["'\s]+;?$/ 216 | */ 217 | css_import?: RegExp 218 | /** 219 | * @default /\$belong\[["'\s]*([^"'\s]+)["'\s]*\]/ 220 | */ 221 | belong?: RegExp 222 | /** 223 | * @default "$[placeholder]" 224 | */ 225 | placeholder?: string 226 | 227 | /** 228 | * 通过文件名判断是否文本资源, 用于include模块扩展mime 229 | */ 230 | isText?: (pathname: string) => boolean 231 | 232 | middlewares?: (MiddlewareCreater | MiddlewareRef)[] 233 | output?: string 234 | /** 235 | * after server create 236 | * you can render websocket server via this 237 | */ 238 | onServerCreate?: (server: Server, conf: F2EConfig) => void 239 | /** 获取环境上下文信息 */ 240 | onContextReady?: (context: { middleware: Middleware, memory: MemoryTree.MemoryTree }) => void 241 | /** 242 | * init urls on server start 243 | */ 244 | init_urls?: string[] 245 | 246 | /** 247 | * 运行时添加(中间件中随时添加),源文件读取,不编译、不输出 248 | * @readonly 249 | */ 250 | __ignores__?: Set 251 | /** 252 | * 输出所有资源编译信息 253 | */ 254 | __withlog__?: boolean 255 | /** 256 | * pages config 257 | */ 258 | page_init?: string 259 | page_404?: string | PageRender<{ pathname: string }> 260 | page_50x?: string | PageRender<{ error: Error }> 261 | page_dir?: string | PageRender<{ pathname: string, dirname: string, store: Object, conf: F2EConfig }> 262 | /** 263 | * 所有响应附加响应头信息 264 | */ 265 | renderHeaders?: { (headers: OutgoingHttpHeaders, req?: IncomingMessage): OutgoingHttpHeaders } 266 | /** 267 | * 提供验证账户密码, 文件上传、删除等操作需要 268 | */ 269 | authorization?: string 270 | 271 | /** 272 | * 自定义全局解析器,默认'memory-tree',不要修改,除非你只打算使用host识别的功能 273 | * @deprecated 274 | */ 275 | app?: 'memory-tree' | { 276 | (conf: F2EConfig): ((req: IncomingMessage, resp: ServerResponse) => void) | Promise<(req: IncomingMessage, resp: ServerResponse) => void> 277 | }, 278 | 279 | /** 280 | * 参考Nginx配置 `try_files` 而产生的功能 (`querystring`已经解析到`req.data`中) 281 | * 1. 类型为`string`时, 所有未能找到资源的情况都转发到这个 `pathname` 282 | * 2. 类型为`{test, exec}[]`, 依次循环匹配`test`, 进行转发 283 | */ 284 | try_files?: string | TryFilesItem[] 285 | 286 | /** 287 | * 统一修改资源名称 288 | * @default `(oldname) => oldname` 289 | */ 290 | rename?: (oldname: string, hash: string) => string 291 | /** 292 | * 资源引用修改名称 293 | */ 294 | namehash?: { 295 | /** 296 | * 要处理的入口文件 297 | * @default ["index\\.html$"] 298 | */ 299 | entries?: string[] 300 | /** 301 | * 替换src的正则 302 | * @default ['\\s(?:=href|src)="([^"]*?)"'] 303 | */ 304 | searchValue?: string[] 305 | /** 306 | * 默认返回 `${output}?${hash}` 307 | * @param output 替换后的文件名 308 | * @param hash 文件摘要md5 309 | * @returns 字符串 310 | * 311 | */ 312 | replacer?: (output: string, hash?: string) => string 313 | } 314 | } 315 | } 316 | export = f2eserver; 317 | -------------------------------------------------------------------------------- /serve/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "f2e-serve", 3 | "version": "0.7.1", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "f2e-serve", 9 | "version": "0.7.1", 10 | "license": "MIT", 11 | "dependencies": { 12 | "f2e-server": "^2.20.13", 13 | "mime": "^3.0.0" 14 | }, 15 | "devDependencies": { 16 | "@types/mime": "^3.0.1", 17 | "@types/node": "^18.11.18" 18 | } 19 | }, 20 | "node_modules/@types/less": { 21 | "version": "3.0.3", 22 | "resolved": "https://registry.npmjs.org/@types/less/-/less-3.0.3.tgz", 23 | "integrity": "sha512-1YXyYH83h6We1djyoUEqTlVyQtCfJAFXELSKW2ZRtjHD4hQ82CC4lvrv5D0l0FLcKBaiPbXyi3MpMsI9ZRgKsw==" 24 | }, 25 | "node_modules/@types/mime": { 26 | "version": "3.0.1", 27 | "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", 28 | "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==", 29 | "dev": true 30 | }, 31 | "node_modules/@types/node": { 32 | "version": "18.11.18", 33 | "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", 34 | "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==", 35 | "dev": true 36 | }, 37 | "node_modules/anymatch": { 38 | "version": "3.1.3", 39 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", 40 | "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", 41 | "dependencies": { 42 | "normalize-path": "^3.0.0", 43 | "picomatch": "^2.0.4" 44 | }, 45 | "engines": { 46 | "node": ">= 8" 47 | } 48 | }, 49 | "node_modules/binary-extensions": { 50 | "version": "2.2.0", 51 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", 52 | "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", 53 | "engines": { 54 | "node": ">=8" 55 | } 56 | }, 57 | "node_modules/braces": { 58 | "version": "3.0.2", 59 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", 60 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", 61 | "dependencies": { 62 | "fill-range": "^7.0.1" 63 | }, 64 | "engines": { 65 | "node": ">=8" 66 | } 67 | }, 68 | "node_modules/chokidar": { 69 | "version": "3.5.3", 70 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", 71 | "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", 72 | "funding": [ 73 | { 74 | "type": "individual", 75 | "url": "https://paulmillr.com/funding/" 76 | } 77 | ], 78 | "dependencies": { 79 | "anymatch": "~3.1.2", 80 | "braces": "~3.0.2", 81 | "glob-parent": "~5.1.2", 82 | "is-binary-path": "~2.1.0", 83 | "is-glob": "~4.0.1", 84 | "normalize-path": "~3.0.0", 85 | "readdirp": "~3.6.0" 86 | }, 87 | "engines": { 88 | "node": ">= 8.10.0" 89 | }, 90 | "optionalDependencies": { 91 | "fsevents": "~2.3.2" 92 | } 93 | }, 94 | "node_modules/commander": { 95 | "version": "2.20.3", 96 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", 97 | "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" 98 | }, 99 | "node_modules/debug": { 100 | "version": "4.3.1", 101 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", 102 | "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", 103 | "dependencies": { 104 | "ms": "2.1.2" 105 | }, 106 | "engines": { 107 | "node": ">=6.0" 108 | }, 109 | "peerDependenciesMeta": { 110 | "supports-color": { 111 | "optional": true 112 | } 113 | } 114 | }, 115 | "node_modules/deep-is": { 116 | "version": "0.1.4", 117 | "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", 118 | "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" 119 | }, 120 | "node_modules/etag": { 121 | "version": "1.8.1", 122 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 123 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", 124 | "engines": { 125 | "node": ">= 0.6" 126 | } 127 | }, 128 | "node_modules/f2e-server": { 129 | "version": "2.20.13", 130 | "resolved": "https://registry.npmjs.org/f2e-server/-/f2e-server-2.20.13.tgz", 131 | "integrity": "sha512-8JmIThqBrfMjn2kQH+pfq+3uzxDc6EZJB0biTVl+Y0Ug5ZmJRWyf6KDqQ/dDem/dgtogHeU8J2Tlw7KJk4dCLg==", 132 | "dependencies": { 133 | "@types/less": "^3.0.2", 134 | "chokidar": "^3.5.3", 135 | "commander": "^2.20.3", 136 | "etag": "^1.8.1", 137 | "lodash": "^4.17.21", 138 | "memory-tree": "^0.6.23", 139 | "mime": "^3.0.0", 140 | "tcp-port-used": "^1.0.2" 141 | }, 142 | "bin": { 143 | "f2e": "bin/f2e" 144 | } 145 | }, 146 | "node_modules/fill-range": { 147 | "version": "7.0.1", 148 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", 149 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", 150 | "dependencies": { 151 | "to-regex-range": "^5.0.1" 152 | }, 153 | "engines": { 154 | "node": ">=8" 155 | } 156 | }, 157 | "node_modules/fsevents": { 158 | "version": "2.3.2", 159 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 160 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 161 | "hasInstallScript": true, 162 | "optional": true, 163 | "os": [ 164 | "darwin" 165 | ], 166 | "engines": { 167 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 168 | } 169 | }, 170 | "node_modules/glob-parent": { 171 | "version": "5.1.2", 172 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 173 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 174 | "dependencies": { 175 | "is-glob": "^4.0.1" 176 | }, 177 | "engines": { 178 | "node": ">= 6" 179 | } 180 | }, 181 | "node_modules/ip-regex": { 182 | "version": "4.3.0", 183 | "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", 184 | "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==", 185 | "engines": { 186 | "node": ">=8" 187 | } 188 | }, 189 | "node_modules/is-binary-path": { 190 | "version": "2.1.0", 191 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 192 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 193 | "dependencies": { 194 | "binary-extensions": "^2.0.0" 195 | }, 196 | "engines": { 197 | "node": ">=8" 198 | } 199 | }, 200 | "node_modules/is-extglob": { 201 | "version": "2.1.1", 202 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 203 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 204 | "engines": { 205 | "node": ">=0.10.0" 206 | } 207 | }, 208 | "node_modules/is-glob": { 209 | "version": "4.0.3", 210 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 211 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 212 | "dependencies": { 213 | "is-extglob": "^2.1.1" 214 | }, 215 | "engines": { 216 | "node": ">=0.10.0" 217 | } 218 | }, 219 | "node_modules/is-number": { 220 | "version": "7.0.0", 221 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 222 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 223 | "engines": { 224 | "node": ">=0.12.0" 225 | } 226 | }, 227 | "node_modules/is-url": { 228 | "version": "1.2.4", 229 | "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", 230 | "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==" 231 | }, 232 | "node_modules/is2": { 233 | "version": "2.0.9", 234 | "resolved": "https://registry.npmjs.org/is2/-/is2-2.0.9.tgz", 235 | "integrity": "sha512-rZkHeBn9Zzq52sd9IUIV3a5mfwBY+o2HePMh0wkGBM4z4qjvy2GwVxQ6nNXSfw6MmVP6gf1QIlWjiOavhM3x5g==", 236 | "dependencies": { 237 | "deep-is": "^0.1.3", 238 | "ip-regex": "^4.1.0", 239 | "is-url": "^1.2.4" 240 | }, 241 | "engines": { 242 | "node": ">=v0.10.0" 243 | } 244 | }, 245 | "node_modules/lodash": { 246 | "version": "4.17.21", 247 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", 248 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" 249 | }, 250 | "node_modules/memory-tree": { 251 | "version": "0.6.23", 252 | "resolved": "https://registry.npmjs.org/memory-tree/-/memory-tree-0.6.23.tgz", 253 | "integrity": "sha512-h7255Vxj70y6e3B4CvCcRY2v+hEpHRJS/ES53bZdQcDSBI2ADK1cLgTgcLRoIMRkghHmZuQ7miVCwSvw8mzgAg==", 254 | "dependencies": { 255 | "chokidar": "^3.5.1", 256 | "lodash": "^4.17.20" 257 | } 258 | }, 259 | "node_modules/mime": { 260 | "version": "3.0.0", 261 | "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", 262 | "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", 263 | "bin": { 264 | "mime": "cli.js" 265 | }, 266 | "engines": { 267 | "node": ">=10.0.0" 268 | } 269 | }, 270 | "node_modules/ms": { 271 | "version": "2.1.2", 272 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 273 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 274 | }, 275 | "node_modules/normalize-path": { 276 | "version": "3.0.0", 277 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 278 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 279 | "engines": { 280 | "node": ">=0.10.0" 281 | } 282 | }, 283 | "node_modules/picomatch": { 284 | "version": "2.3.1", 285 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 286 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 287 | "engines": { 288 | "node": ">=8.6" 289 | }, 290 | "funding": { 291 | "url": "https://github.com/sponsors/jonschlinkert" 292 | } 293 | }, 294 | "node_modules/readdirp": { 295 | "version": "3.6.0", 296 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", 297 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", 298 | "dependencies": { 299 | "picomatch": "^2.2.1" 300 | }, 301 | "engines": { 302 | "node": ">=8.10.0" 303 | } 304 | }, 305 | "node_modules/tcp-port-used": { 306 | "version": "1.0.2", 307 | "resolved": "https://registry.npmjs.org/tcp-port-used/-/tcp-port-used-1.0.2.tgz", 308 | "integrity": "sha512-l7ar8lLUD3XS1V2lfoJlCBaeoaWo/2xfYt81hM7VlvR4RrMVFqfmzfhLVk40hAb368uitje5gPtBRL1m/DGvLA==", 309 | "dependencies": { 310 | "debug": "4.3.1", 311 | "is2": "^2.0.6" 312 | } 313 | }, 314 | "node_modules/to-regex-range": { 315 | "version": "5.0.1", 316 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 317 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 318 | "dependencies": { 319 | "is-number": "^7.0.0" 320 | }, 321 | "engines": { 322 | "node": ">=8.0" 323 | } 324 | } 325 | }, 326 | "dependencies": { 327 | "@types/less": { 328 | "version": "3.0.3", 329 | "resolved": "https://registry.npmjs.org/@types/less/-/less-3.0.3.tgz", 330 | "integrity": "sha512-1YXyYH83h6We1djyoUEqTlVyQtCfJAFXELSKW2ZRtjHD4hQ82CC4lvrv5D0l0FLcKBaiPbXyi3MpMsI9ZRgKsw==" 331 | }, 332 | "@types/mime": { 333 | "version": "3.0.1", 334 | "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", 335 | "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==", 336 | "dev": true 337 | }, 338 | "@types/node": { 339 | "version": "18.11.18", 340 | "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", 341 | "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==", 342 | "dev": true 343 | }, 344 | "anymatch": { 345 | "version": "3.1.3", 346 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", 347 | "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", 348 | "requires": { 349 | "normalize-path": "^3.0.0", 350 | "picomatch": "^2.0.4" 351 | } 352 | }, 353 | "binary-extensions": { 354 | "version": "2.2.0", 355 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", 356 | "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" 357 | }, 358 | "braces": { 359 | "version": "3.0.2", 360 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", 361 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", 362 | "requires": { 363 | "fill-range": "^7.0.1" 364 | } 365 | }, 366 | "chokidar": { 367 | "version": "3.5.3", 368 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", 369 | "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", 370 | "requires": { 371 | "anymatch": "~3.1.2", 372 | "braces": "~3.0.2", 373 | "fsevents": "~2.3.2", 374 | "glob-parent": "~5.1.2", 375 | "is-binary-path": "~2.1.0", 376 | "is-glob": "~4.0.1", 377 | "normalize-path": "~3.0.0", 378 | "readdirp": "~3.6.0" 379 | } 380 | }, 381 | "commander": { 382 | "version": "2.20.3", 383 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", 384 | "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" 385 | }, 386 | "debug": { 387 | "version": "4.3.1", 388 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", 389 | "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", 390 | "requires": { 391 | "ms": "2.1.2" 392 | } 393 | }, 394 | "deep-is": { 395 | "version": "0.1.4", 396 | "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", 397 | "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" 398 | }, 399 | "etag": { 400 | "version": "1.8.1", 401 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 402 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" 403 | }, 404 | "f2e-server": { 405 | "version": "2.20.13", 406 | "resolved": "https://registry.npmjs.org/f2e-server/-/f2e-server-2.20.13.tgz", 407 | "integrity": "sha512-8JmIThqBrfMjn2kQH+pfq+3uzxDc6EZJB0biTVl+Y0Ug5ZmJRWyf6KDqQ/dDem/dgtogHeU8J2Tlw7KJk4dCLg==", 408 | "requires": { 409 | "@types/less": "^3.0.2", 410 | "chokidar": "^3.5.3", 411 | "commander": "^2.20.3", 412 | "etag": "^1.8.1", 413 | "lodash": "^4.17.21", 414 | "memory-tree": "^0.6.23", 415 | "mime": "^3.0.0", 416 | "tcp-port-used": "^1.0.2" 417 | } 418 | }, 419 | "fill-range": { 420 | "version": "7.0.1", 421 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", 422 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", 423 | "requires": { 424 | "to-regex-range": "^5.0.1" 425 | } 426 | }, 427 | "fsevents": { 428 | "version": "2.3.2", 429 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 430 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 431 | "optional": true 432 | }, 433 | "glob-parent": { 434 | "version": "5.1.2", 435 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 436 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 437 | "requires": { 438 | "is-glob": "^4.0.1" 439 | } 440 | }, 441 | "ip-regex": { 442 | "version": "4.3.0", 443 | "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", 444 | "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==" 445 | }, 446 | "is-binary-path": { 447 | "version": "2.1.0", 448 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 449 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 450 | "requires": { 451 | "binary-extensions": "^2.0.0" 452 | } 453 | }, 454 | "is-extglob": { 455 | "version": "2.1.1", 456 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 457 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" 458 | }, 459 | "is-glob": { 460 | "version": "4.0.3", 461 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 462 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 463 | "requires": { 464 | "is-extglob": "^2.1.1" 465 | } 466 | }, 467 | "is-number": { 468 | "version": "7.0.0", 469 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 470 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" 471 | }, 472 | "is-url": { 473 | "version": "1.2.4", 474 | "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", 475 | "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==" 476 | }, 477 | "is2": { 478 | "version": "2.0.9", 479 | "resolved": "https://registry.npmjs.org/is2/-/is2-2.0.9.tgz", 480 | "integrity": "sha512-rZkHeBn9Zzq52sd9IUIV3a5mfwBY+o2HePMh0wkGBM4z4qjvy2GwVxQ6nNXSfw6MmVP6gf1QIlWjiOavhM3x5g==", 481 | "requires": { 482 | "deep-is": "^0.1.3", 483 | "ip-regex": "^4.1.0", 484 | "is-url": "^1.2.4" 485 | } 486 | }, 487 | "lodash": { 488 | "version": "4.17.21", 489 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", 490 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" 491 | }, 492 | "memory-tree": { 493 | "version": "0.6.23", 494 | "resolved": "https://registry.npmjs.org/memory-tree/-/memory-tree-0.6.23.tgz", 495 | "integrity": "sha512-h7255Vxj70y6e3B4CvCcRY2v+hEpHRJS/ES53bZdQcDSBI2ADK1cLgTgcLRoIMRkghHmZuQ7miVCwSvw8mzgAg==", 496 | "requires": { 497 | "chokidar": "^3.5.1", 498 | "lodash": "^4.17.20" 499 | } 500 | }, 501 | "mime": { 502 | "version": "3.0.0", 503 | "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", 504 | "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==" 505 | }, 506 | "ms": { 507 | "version": "2.1.2", 508 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 509 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 510 | }, 511 | "normalize-path": { 512 | "version": "3.0.0", 513 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 514 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" 515 | }, 516 | "picomatch": { 517 | "version": "2.3.1", 518 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 519 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" 520 | }, 521 | "readdirp": { 522 | "version": "3.6.0", 523 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", 524 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", 525 | "requires": { 526 | "picomatch": "^2.2.1" 527 | } 528 | }, 529 | "tcp-port-used": { 530 | "version": "1.0.2", 531 | "resolved": "https://registry.npmjs.org/tcp-port-used/-/tcp-port-used-1.0.2.tgz", 532 | "integrity": "sha512-l7ar8lLUD3XS1V2lfoJlCBaeoaWo/2xfYt81hM7VlvR4RrMVFqfmzfhLVk40hAb368uitje5gPtBRL1m/DGvLA==", 533 | "requires": { 534 | "debug": "4.3.1", 535 | "is2": "^2.0.6" 536 | } 537 | }, 538 | "to-regex-range": { 539 | "version": "5.0.1", 540 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 541 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 542 | "requires": { 543 | "is-number": "^7.0.0" 544 | } 545 | } 546 | } 547 | } 548 | --------------------------------------------------------------------------------