├── .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 |
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 |
--------------------------------------------------------------------------------