@srfnstack/spliffy 0.5.5 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +0 -0
- package/README.md +0 -0
- package/package.json +14 -6
- package/src/content-types.mjs +75 -0
- package/src/content.mjs +95 -0
- package/src/decorator.mjs +205 -0
- package/src/handler.mjs +240 -0
- package/src/httpStatusCodes.mjs +103 -0
- package/src/{index.js → index.mjs} +14 -18
- package/src/log.mjs +59 -0
- package/src/middleware.mjs +90 -0
- package/src/nodeModuleHandler.mjs +61 -0
- package/src/routes.mjs +180 -0
- package/src/server.mjs +126 -0
- package/src/serverConfig.mjs +84 -0
- package/src/start.mjs +25 -0
- package/src/staticHandler.mjs +74 -0
- package/src/url.mjs +24 -0
- package/src/content-types.js +0 -75
- package/src/content.js +0 -56
- package/src/dispatcher.js +0 -190
- package/src/expressShim.js +0 -120
- package/src/log.js +0 -43
- package/src/middleware.js +0 -121
- package/src/nodeModuleHandler.js +0 -54
- package/src/parseUrl.js +0 -43
- package/src/routeUtil.js +0 -67
- package/src/routes.js +0 -191
- package/src/secure.js +0 -53
- package/src/serverConfig.js +0 -70
- package/src/start.js +0 -39
- package/src/staticHandler.js +0 -115
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
export const defaultStatusMessages = {
|
|
2
|
+
200: 'OK',
|
|
3
|
+
201: 'Created',
|
|
4
|
+
202: 'Accepted',
|
|
5
|
+
203: 'Non-Authoritative Information',
|
|
6
|
+
204: 'No Content',
|
|
7
|
+
205: 'Reset Content',
|
|
8
|
+
206: 'Partial Content',
|
|
9
|
+
300: 'Multiple Choice',
|
|
10
|
+
301: 'Moved Permanently',
|
|
11
|
+
302: 'Found',
|
|
12
|
+
303: 'See Other',
|
|
13
|
+
304: 'Not Modified',
|
|
14
|
+
307: 'Temporary Redirect',
|
|
15
|
+
308: 'Permanent Redirect',
|
|
16
|
+
400: 'Bad Request',
|
|
17
|
+
401: 'Unauthorized',
|
|
18
|
+
402: 'Payment Required',
|
|
19
|
+
403: 'Forbidden',
|
|
20
|
+
404: 'Not Found',
|
|
21
|
+
405: 'Method Not Allowed',
|
|
22
|
+
406: 'Not Acceptable',
|
|
23
|
+
407: 'Proxy Authentication Required',
|
|
24
|
+
408: 'Request Timeout',
|
|
25
|
+
409: 'Conflict',
|
|
26
|
+
410: 'Gone',
|
|
27
|
+
411: 'Length Required',
|
|
28
|
+
412: 'Precondition Failed',
|
|
29
|
+
413: 'Payload Too Large',
|
|
30
|
+
414: 'URI Too Long',
|
|
31
|
+
415: 'Unsupported Media Type',
|
|
32
|
+
416: 'Range Not Satisfiable',
|
|
33
|
+
417: 'Expectation Failed',
|
|
34
|
+
418: 'I\'m a teapot',
|
|
35
|
+
420: 'Enhance Your Calm',
|
|
36
|
+
425: 'Too Early',
|
|
37
|
+
426: 'Upgrade Required',
|
|
38
|
+
428: 'Precondition Required',
|
|
39
|
+
429: 'Too Many Requests',
|
|
40
|
+
431: 'Request Header Fields Too Large',
|
|
41
|
+
451: 'Unavailable For Legal Reasons',
|
|
42
|
+
500: 'Internal Server Error',
|
|
43
|
+
501: 'Not Implemented',
|
|
44
|
+
502: 'Bad Gateway',
|
|
45
|
+
503: 'Service Unavailable',
|
|
46
|
+
504: 'Gateway Timeout',
|
|
47
|
+
505: 'Http Version Not Supported',
|
|
48
|
+
506: 'Variant Also Negotiates',
|
|
49
|
+
510: 'Not Extended',
|
|
50
|
+
511: 'Network Authentication Required'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export default {
|
|
54
|
+
OK: 200,
|
|
55
|
+
CREATED: 201,
|
|
56
|
+
ACCEPTED: 202,
|
|
57
|
+
NON_AUTHORITATIVE_INFORMATION: 203,
|
|
58
|
+
NO_CONTENT: 204,
|
|
59
|
+
RESET_CONTENT: 205,
|
|
60
|
+
PARTIAL_CONTENT: 206,
|
|
61
|
+
MULTIPLE_CHOICE: 300,
|
|
62
|
+
MOVED_PERMANENTLY: 301,
|
|
63
|
+
FOUND: 302,
|
|
64
|
+
SEE_OTHER: 303,
|
|
65
|
+
NOT_MODIFIED: 304,
|
|
66
|
+
TEMPORARY_REDIRECT: 307,
|
|
67
|
+
PERMANENT_REDIRECT: 308,
|
|
68
|
+
BAD_REQUEST: 400,
|
|
69
|
+
UNAUTHORIZED: 401,
|
|
70
|
+
PAYMENT_REQUIRED: 402,
|
|
71
|
+
FORBIDDEN: 403,
|
|
72
|
+
NOT_FOUND: 404,
|
|
73
|
+
METHOD_NOT_ALLOWED: 405,
|
|
74
|
+
NOT_ACCEPTABLE: 406,
|
|
75
|
+
PROXY_AUTHENTICATION_REQUIRED: 407,
|
|
76
|
+
REQUEST_TIMEOUT: 408,
|
|
77
|
+
CONFLICT: 409,
|
|
78
|
+
GONE: 410,
|
|
79
|
+
LENGTH_REQUIRED: 411,
|
|
80
|
+
PRECONDITION_FAILED: 412,
|
|
81
|
+
PAYLOAD_TOO_LARGE: 413,
|
|
82
|
+
URI_TOO_LONG: 414,
|
|
83
|
+
UNSUPPORTED_MEDIA_TYPE: 415,
|
|
84
|
+
RANGE_NOT_SATISFIABLE: 416,
|
|
85
|
+
EXPECTATION_FAILED: 417,
|
|
86
|
+
IM_A_TEAPOT: 418,
|
|
87
|
+
ENHANCE_YOUR_CALM: 420,
|
|
88
|
+
TOO_EARLY: 425,
|
|
89
|
+
UPGRADE_REQUIRED: 426,
|
|
90
|
+
PRECONDITION_REQUIRED: 428,
|
|
91
|
+
TOO_MANY_REQUESTS: 429,
|
|
92
|
+
REQUEST_HEADER_FIELDS_TOO_LARGE: 431,
|
|
93
|
+
UNAVAILABLE_FOR_LEGAL_REASONS: 451,
|
|
94
|
+
INTERNAL_SERVER_ERROR: 500,
|
|
95
|
+
NOT_IMPLEMENTED: 501,
|
|
96
|
+
BAD_GATEWAY: 502,
|
|
97
|
+
SERVICE_UNAVAILABLE: 503,
|
|
98
|
+
GATEWAY_TIMEOUT: 504,
|
|
99
|
+
HTTP_VERSION_NOT_SUPPORTED: 505,
|
|
100
|
+
VARIANT_ALSO_NEGOTIATES: 506,
|
|
101
|
+
NOT_EXTENDED: 510,
|
|
102
|
+
NETWORK_AUTHENTICATION_REQUIRED: 511
|
|
103
|
+
}
|
|
@@ -1,25 +1,21 @@
|
|
|
1
|
-
const log = require( './log' )
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Startup function for the spliffy server
|
|
5
|
-
* Startup will exponentially back off on consecutive failures
|
|
6
|
-
* @param config See https://github.com/narcolepticsnowman/spliffy#config
|
|
7
|
-
* @returns {Promise<Server>} Either the https server if https is configured or the http server
|
|
8
|
-
*/
|
|
9
|
-
const spliffy = ( config ) => require( './start' )( config )
|
|
10
|
-
|
|
11
1
|
/**
|
|
12
2
|
* A helper for creating a redirect handler
|
|
13
3
|
* @param location The location to redirect to
|
|
14
4
|
* @param permanent Whether this is a permanent redirect or not
|
|
15
5
|
*/
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
6
|
+
export const redirect = (location, permanent = true) => () => ({
|
|
7
|
+
statusCode: permanent ? 301 : 302,
|
|
8
|
+
headers: {
|
|
9
|
+
location: location
|
|
10
|
+
}
|
|
11
|
+
})
|
|
22
12
|
|
|
23
|
-
|
|
13
|
+
export { default as log } from './log.mjs'
|
|
14
|
+
export { parseQuery, setMultiValueKey } from './url.mjs'
|
|
24
15
|
|
|
25
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Startup function for the spliffy server
|
|
18
|
+
* @param config See https://github.com/narcolepticsnowman/spliffy#config
|
|
19
|
+
* @returns {Promise<Server>} Either the https server if https is configured or the http server
|
|
20
|
+
*/
|
|
21
|
+
export { default } from './start.mjs'
|
package/src/log.mjs
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import util from 'util'
|
|
2
|
+
|
|
3
|
+
const inspect = util.inspect
|
|
4
|
+
const levelOrder = { TRACE: 10, DEBUG: 20, INFO: 30, ACCESS: 30, 'GOOD NEWS EVERYONE!': 30, WARN: 40, ERROR: 50, FATAL: 60, NONE: 100 }
|
|
5
|
+
let logLevel = levelOrder.INFO
|
|
6
|
+
|
|
7
|
+
const ifLevelEnabled = (fn, level, args) => {
|
|
8
|
+
const configLevel = levelOrder[logLevel] || levelOrder.INFO
|
|
9
|
+
if (levelOrder[level] >= configLevel) {
|
|
10
|
+
fn(`[${new Date().toISOString()}] [${level}] ${args.map(a => typeof a === 'string' ? a : inspect(a, { depth: null })).join(' ')}`)
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const callLog = (level, logImplFn, defaultFn, args) => {
|
|
15
|
+
if (logImpl && typeof logImpl[logImplFn] === 'function') {
|
|
16
|
+
logImpl[logImplFn](...args)
|
|
17
|
+
} else {
|
|
18
|
+
ifLevelEnabled(defaultFn, level, args)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let logImpl = null
|
|
23
|
+
|
|
24
|
+
export default {
|
|
25
|
+
setLogLevel (level) {
|
|
26
|
+
level = level.toUpperCase()
|
|
27
|
+
if (!(level in levelOrder)) {
|
|
28
|
+
throw new Error(`Invalid level: ${level}`)
|
|
29
|
+
}
|
|
30
|
+
logLevel = level
|
|
31
|
+
},
|
|
32
|
+
setLogger (logger) {
|
|
33
|
+
logImpl = logger
|
|
34
|
+
},
|
|
35
|
+
trace () {
|
|
36
|
+
callLog('TRACE', 'trace', console.trace, [...arguments])
|
|
37
|
+
},
|
|
38
|
+
debug () {
|
|
39
|
+
callLog('DEBUG', 'debug', console.debug, [...arguments])
|
|
40
|
+
},
|
|
41
|
+
info () {
|
|
42
|
+
callLog('INFO', 'info', console.info, [...arguments])
|
|
43
|
+
},
|
|
44
|
+
gne () {
|
|
45
|
+
callLog('GOOD NEWS EVERYONE!', 'info', console.info, [...arguments])
|
|
46
|
+
},
|
|
47
|
+
access () {
|
|
48
|
+
callLog('ACCESS', 'info', console.info, [...arguments])
|
|
49
|
+
},
|
|
50
|
+
warn () {
|
|
51
|
+
callLog('WARN', 'warn', console.warn, [...arguments])
|
|
52
|
+
},
|
|
53
|
+
error () {
|
|
54
|
+
callLog('ERROR', 'error', console.error, [...arguments].map(arg => arg.stack ? arg.stack : arg))
|
|
55
|
+
},
|
|
56
|
+
fatal () {
|
|
57
|
+
callLog('ERROR', 'error', console.error, [...arguments].map(arg => arg.stack ? arg.stack : arg))
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import log from './log.mjs'
|
|
2
|
+
/**
|
|
3
|
+
* middleware is stored as an object where the properties are request methods the middleware applies to
|
|
4
|
+
* if a middleware applies to all methods, the property ALL is used
|
|
5
|
+
* example:
|
|
6
|
+
* {
|
|
7
|
+
* GET: [(req,res,next)=>console.log('ice cream man')]
|
|
8
|
+
* POST: [(req,res,next)=>console.log('gelato')]
|
|
9
|
+
* ALL: [(req,res,next)=>console.log('bruce banner')]
|
|
10
|
+
* }
|
|
11
|
+
*/
|
|
12
|
+
export const mergeMiddleware = (incoming, existing) => {
|
|
13
|
+
const mergeInto = cloneMiddleware(existing)
|
|
14
|
+
|
|
15
|
+
validateMiddleware(incoming)
|
|
16
|
+
if (Array.isArray(incoming)) {
|
|
17
|
+
mergeInto.ALL = (existing.ALL || []).concat(incoming)
|
|
18
|
+
} else if (typeof incoming === 'object') {
|
|
19
|
+
for (const method in incoming) {
|
|
20
|
+
const upMethod = method.toUpperCase()
|
|
21
|
+
mergeInto[upMethod] = (mergeInto[method] || []).concat(incoming[upMethod] || [])
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return mergeInto
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const cloneMiddleware = (middleware) => {
|
|
28
|
+
const clone = { ...middleware }
|
|
29
|
+
for (const method in middleware) {
|
|
30
|
+
clone[method] = [...(middleware[method] || [])]
|
|
31
|
+
}
|
|
32
|
+
return clone
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Ensure the given middleware is valid
|
|
37
|
+
* @param middleware
|
|
38
|
+
*/
|
|
39
|
+
export const validateMiddleware = (middleware) => {
|
|
40
|
+
if (Array.isArray(middleware)) {
|
|
41
|
+
validateMiddlewareArray(middleware)
|
|
42
|
+
} else if (typeof middleware === 'object') {
|
|
43
|
+
for (const method in middleware) {
|
|
44
|
+
// ensure methods are always available as uppercase
|
|
45
|
+
const upMethod = method.toUpperCase()
|
|
46
|
+
middleware[upMethod] = middleware[method]
|
|
47
|
+
validateMiddlewareArray(middleware[upMethod])
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
throw new Error('Invalid middleware definition: ' + middleware)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const validateMiddlewareArray = (arr) => {
|
|
55
|
+
if (!Array.isArray(arr)) {
|
|
56
|
+
throw new Error('middleware must be an array of functions')
|
|
57
|
+
}
|
|
58
|
+
for (const f of arr) {
|
|
59
|
+
if (typeof f !== 'function') {
|
|
60
|
+
throw new Error('Each element in the array of middleware must be a function')
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function invokeMiddleware (middleware, req, res, reqErr) {
|
|
66
|
+
await new Promise((resolve, reject) => {
|
|
67
|
+
let current = -1
|
|
68
|
+
const next = (err) => {
|
|
69
|
+
if (err) reject(err)
|
|
70
|
+
if (res.writableEnded) {
|
|
71
|
+
resolve()
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
current++
|
|
75
|
+
if (current === middleware.length) {
|
|
76
|
+
resolve()
|
|
77
|
+
} else {
|
|
78
|
+
try {
|
|
79
|
+
if (reqErr) middleware[current](reqErr, req, res, next)
|
|
80
|
+
else middleware[current](req, res, next)
|
|
81
|
+
} catch (e) {
|
|
82
|
+
log.error('Middleware threw exception', e)
|
|
83
|
+
reject(e)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
next()
|
|
89
|
+
})
|
|
90
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { mergeMiddleware } from './middleware.mjs'
|
|
4
|
+
import { createStaticHandler } from './staticHandler.mjs'
|
|
5
|
+
import { getContentTypeByExtension } from './content.mjs'
|
|
6
|
+
|
|
7
|
+
const stripLeadingSlash = p => p.startsWith('/') ? p.substr(1) : p
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
This method will add all of the configured node_module files to the given routes.
|
|
11
|
+
The configured node moduleRoutes must be explicit files, no pattern matching is supported.
|
|
12
|
+
Generating the list of files using pattern matching yourself is highly discouraged.
|
|
13
|
+
It is much safer to explicitly list every file you wish to be served so you don't inadvertently serve additional files.
|
|
14
|
+
*/
|
|
15
|
+
export function getNodeModuleRoutes (config) {
|
|
16
|
+
const nodeModuleRoutes = config.nodeModuleRoutes
|
|
17
|
+
const routes = []
|
|
18
|
+
if (nodeModuleRoutes && typeof nodeModuleRoutes === 'object') {
|
|
19
|
+
const nodeModulesDir = nodeModuleRoutes.nodeModulesPath ? path.resolve(nodeModuleRoutes.nodeModulesPath) : path.resolve(config.routeDir, '..', 'node_modules')
|
|
20
|
+
if (!fs.existsSync(nodeModulesDir)) {
|
|
21
|
+
throw new Error(`Unable to find node_modules dir at ${nodeModulesDir}`)
|
|
22
|
+
}
|
|
23
|
+
const prefix = stripLeadingSlash(nodeModuleRoutes.routePrefix || 'lib')
|
|
24
|
+
if (!Array.isArray(nodeModuleRoutes.files)) {
|
|
25
|
+
nodeModuleRoutes.files = [nodeModuleRoutes.files]
|
|
26
|
+
}
|
|
27
|
+
for (const file of nodeModuleRoutes.files) {
|
|
28
|
+
let filePath, urlPath
|
|
29
|
+
if (file && typeof file === 'object') {
|
|
30
|
+
filePath = path.join(nodeModulesDir, file.modulePath)
|
|
31
|
+
urlPath = `/${prefix}/${stripLeadingSlash(file.urlPath || file.modulePath)}`
|
|
32
|
+
} else if (file && typeof file === 'string') {
|
|
33
|
+
filePath = path.join(nodeModulesDir, file)
|
|
34
|
+
urlPath = `/${prefix}/${stripLeadingSlash(file)}`
|
|
35
|
+
} else {
|
|
36
|
+
throw new Error('Invalid node_module file: ' + file)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (fs.existsSync(filePath)) {
|
|
40
|
+
const parts = urlPath.split('/')
|
|
41
|
+
const lastPart = parts.pop()
|
|
42
|
+
const mw = {}
|
|
43
|
+
mergeMiddleware(config.middleware, mw)
|
|
44
|
+
mergeMiddleware(nodeModuleRoutes.middleware || {}, mw)
|
|
45
|
+
routes.push({
|
|
46
|
+
pathParameters: [],
|
|
47
|
+
urlPath,
|
|
48
|
+
filePath,
|
|
49
|
+
handlers: createStaticHandler(
|
|
50
|
+
filePath, getContentTypeByExtension(lastPart, config.staticContentTypes),
|
|
51
|
+
config.cacheStatic, config.staticCacheControl
|
|
52
|
+
),
|
|
53
|
+
middleware: mw
|
|
54
|
+
})
|
|
55
|
+
} else {
|
|
56
|
+
console.warn(`The specified node_modules file: ${file} does not exist and will not be served.`)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return routes
|
|
61
|
+
}
|
package/src/routes.mjs
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { validateMiddleware, mergeMiddleware } from './middleware.mjs'
|
|
2
|
+
import { createStaticHandler } from './staticHandler.mjs'
|
|
3
|
+
import { getContentTypeByExtension } from './content.mjs'
|
|
4
|
+
import fs from 'fs'
|
|
5
|
+
import path from 'path'
|
|
6
|
+
import { HTTP_METHODS } from './handler.mjs'
|
|
7
|
+
import util from 'util'
|
|
8
|
+
|
|
9
|
+
const { promisify } = util
|
|
10
|
+
|
|
11
|
+
const readdir = promisify(fs.readdir)
|
|
12
|
+
|
|
13
|
+
const isVariable = part => part.startsWith('$')
|
|
14
|
+
const getVariableName = part => part.substr(1)
|
|
15
|
+
const getPathPart = name => {
|
|
16
|
+
if (name === 'index') {
|
|
17
|
+
return ''
|
|
18
|
+
}
|
|
19
|
+
if (name.startsWith('$')) {
|
|
20
|
+
return `:${name.substr(1)}`
|
|
21
|
+
} else if (name.endsWith('+')) {
|
|
22
|
+
return `${name.substr(0, name.length - 1)}/*`
|
|
23
|
+
} else {
|
|
24
|
+
return name
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const filterTestFiles = config => f => (!f.name.endsWith('.test.js') && !f.name.endsWith('.test.mjs')) || config.allowTestFileRoutes
|
|
28
|
+
const filterIgnoredFiles = config => f => !config.ignoreFilesMatching.filter(p => p).find(pattern => f.name.match(pattern))
|
|
29
|
+
const ignoreHandlerFields = { middleware: true, streamRequestBody: true }
|
|
30
|
+
const doFindRoutes = async (config, currentFile, filePath, urlPath, pathParameters, inheritedMiddleware) => {
|
|
31
|
+
const routes = []
|
|
32
|
+
const name = currentFile.name
|
|
33
|
+
if (currentFile.isDirectory()) {
|
|
34
|
+
routes.push(...(await findRoutesInDir(name, filePath, urlPath, inheritedMiddleware, pathParameters, config)))
|
|
35
|
+
} else if (!config.staticMode && (name.endsWith('.rt.js') || name.endsWith('.rt.mjs'))) {
|
|
36
|
+
routes.push(await buildJSHandlerRoute(name, filePath, urlPath, inheritedMiddleware, pathParameters))
|
|
37
|
+
} else {
|
|
38
|
+
routes.push(...buildStaticRoutes(name, filePath, urlPath, inheritedMiddleware, pathParameters, config))
|
|
39
|
+
}
|
|
40
|
+
return routes
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const importModules = async (config, dirPath, files) => Promise.all(
|
|
44
|
+
files
|
|
45
|
+
.filter(filterTestFiles(config))
|
|
46
|
+
.filter(filterIgnoredFiles(config))
|
|
47
|
+
.map(f => path.join(dirPath, f.name))
|
|
48
|
+
.map(mwPath => import(`file://${mwPath}`)
|
|
49
|
+
.then(module => ({ module, mwPath }))
|
|
50
|
+
))
|
|
51
|
+
|
|
52
|
+
const findRoutesInDir = async (name, filePath, urlPath, inheritedMiddleware, pathParameters, config) => {
|
|
53
|
+
if (isVariable(name)) {
|
|
54
|
+
pathParameters = pathParameters.concat(getVariableName(name))
|
|
55
|
+
}
|
|
56
|
+
const files = await readdir(filePath, { withFileTypes: true })
|
|
57
|
+
|
|
58
|
+
const middlewareModules = await importModules(config, filePath,
|
|
59
|
+
files.filter(f => f.name.endsWith('.mw.js') || f.name.endsWith('.mw.mjs'))
|
|
60
|
+
)
|
|
61
|
+
const dirMiddleware = middlewareModules.map(({ module, mwPath }) => {
|
|
62
|
+
const middleware = module.middleware || module.default?.middleware
|
|
63
|
+
if (!middleware) {
|
|
64
|
+
throw new Error(`${mwPath} must export a middleware property or have a middleware property on the default export`)
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
validateMiddleware(middleware)
|
|
68
|
+
} catch (e) {
|
|
69
|
+
throw new Error('Failed to load middleware in file ' + mwPath + '\n' + e.message + '\n' + e.stack)
|
|
70
|
+
}
|
|
71
|
+
return middleware
|
|
72
|
+
})
|
|
73
|
+
.reduce((result, incoming) => mergeMiddleware(incoming, result), inheritedMiddleware)
|
|
74
|
+
|
|
75
|
+
return Promise.all(files
|
|
76
|
+
.filter(f => !f.name.endsWith('.mw.js') && !f.name.endsWith('.mw.mjs'))
|
|
77
|
+
.filter(filterTestFiles(config))
|
|
78
|
+
.filter(filterIgnoredFiles(config))
|
|
79
|
+
.map(
|
|
80
|
+
(f) => doFindRoutes(
|
|
81
|
+
config,
|
|
82
|
+
f,
|
|
83
|
+
path.join(filePath, f.name),
|
|
84
|
+
urlPath + '/' + getPathPart(name),
|
|
85
|
+
pathParameters,
|
|
86
|
+
dirMiddleware
|
|
87
|
+
)
|
|
88
|
+
))
|
|
89
|
+
.then(routes => routes.flat())
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const buildJSHandlerRoute = async (name, filePath, urlPath, inheritedMiddleware, pathParameters) => {
|
|
93
|
+
if (name.endsWith('.mjs')) {
|
|
94
|
+
name = name.substr(0, name.length - '.rt.mjs'.length)
|
|
95
|
+
} else {
|
|
96
|
+
name = name.substr(0, name.length - '.rt.js'.length)
|
|
97
|
+
}
|
|
98
|
+
if (isVariable(name)) {
|
|
99
|
+
pathParameters = pathParameters.concat(getVariableName(name))
|
|
100
|
+
}
|
|
101
|
+
const route = {
|
|
102
|
+
pathParameters,
|
|
103
|
+
urlPath: `${urlPath}/${getPathPart(name)}`,
|
|
104
|
+
filePath,
|
|
105
|
+
handlers: {}
|
|
106
|
+
}
|
|
107
|
+
const module = await import(`file://${filePath}`)
|
|
108
|
+
const handlers = module.default
|
|
109
|
+
|
|
110
|
+
route.middleware = mergeMiddleware(handlers.middleware || [], inheritedMiddleware)
|
|
111
|
+
for (const method of Object.keys(handlers).filter(k => !ignoreHandlerFields[k])) {
|
|
112
|
+
if (HTTP_METHODS.indexOf(method) === -1) {
|
|
113
|
+
throw new Error(`Method: ${method} in file ${filePath} is not a valid http method. It must be one of: ${HTTP_METHODS}. Method names must be all uppercase.`)
|
|
114
|
+
}
|
|
115
|
+
const loadedHandler = handlers[method]
|
|
116
|
+
let handler = loadedHandler
|
|
117
|
+
if (typeof loadedHandler.handler === 'function') {
|
|
118
|
+
handler = loadedHandler.handler
|
|
119
|
+
}
|
|
120
|
+
if (typeof handler !== 'function') {
|
|
121
|
+
throw new Error(`Request method ${method} in file ${filePath} must be a function. Got: ${handlers[method]}`)
|
|
122
|
+
}
|
|
123
|
+
if (!('streamRequestBody' in loadedHandler)) {
|
|
124
|
+
handler.streamRequestBody = handlers.streamRequestBody
|
|
125
|
+
} else {
|
|
126
|
+
handler.streamRequestBody = loadedHandler.streamRequestBody
|
|
127
|
+
}
|
|
128
|
+
route.handlers[method] = handler
|
|
129
|
+
}
|
|
130
|
+
return route
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const buildStaticRoutes = (name, filePath, urlPath, inheritedMiddleware, pathParameters, config) => {
|
|
134
|
+
const routes = []
|
|
135
|
+
if (isVariable(name)) {
|
|
136
|
+
pathParameters = pathParameters.concat(getVariableName(name))
|
|
137
|
+
}
|
|
138
|
+
const contentType = getContentTypeByExtension(name, config.staticContentTypes)
|
|
139
|
+
const route = {
|
|
140
|
+
pathParameters,
|
|
141
|
+
urlPath: `${urlPath}/${getPathPart(name)}`,
|
|
142
|
+
filePath,
|
|
143
|
+
handlers: createStaticHandler(filePath, contentType, config.cacheStatic, config.staticCacheControl),
|
|
144
|
+
middleware: inheritedMiddleware
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
routes.push(route)
|
|
148
|
+
|
|
149
|
+
for (const ext of config.resolveWithoutExtension) {
|
|
150
|
+
if (name.endsWith(ext)) {
|
|
151
|
+
const strippedName = name.substr(0, name.length - ext.length)
|
|
152
|
+
// in the index case we need to add both the stripped and an empty path so it will resolve the parent
|
|
153
|
+
if (strippedName === 'index') {
|
|
154
|
+
const noExtRoute = Object.assign({}, route)
|
|
155
|
+
noExtRoute.urlPath = `${urlPath}/${strippedName}`
|
|
156
|
+
routes.push(noExtRoute)
|
|
157
|
+
}
|
|
158
|
+
const noExtRoute = Object.assign({}, route)
|
|
159
|
+
noExtRoute.urlPath = `${urlPath}/${getPathPart(strippedName)}`
|
|
160
|
+
routes.push(noExtRoute)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return routes
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function findRoutes (config) {
|
|
167
|
+
const fullRouteDir = path.resolve(config.routeDir)
|
|
168
|
+
if (!fs.existsSync(fullRouteDir)) {
|
|
169
|
+
throw new Error(`can't find route directory: ${fullRouteDir}`)
|
|
170
|
+
}
|
|
171
|
+
const appMiddleware = mergeMiddleware(config.middleware || [], {})
|
|
172
|
+
const files = await readdir(fullRouteDir, { withFileTypes: true })
|
|
173
|
+
return Promise.all(files
|
|
174
|
+
.filter(filterTestFiles(config))
|
|
175
|
+
.filter(filterIgnoredFiles(config))
|
|
176
|
+
.map(
|
|
177
|
+
f => doFindRoutes(config, f, path.join(fullRouteDir, f.name), '', [], appMiddleware)
|
|
178
|
+
))
|
|
179
|
+
.then(routes => routes.flat())
|
|
180
|
+
}
|
package/src/server.mjs
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import log from './log.mjs'
|
|
2
|
+
import { getNodeModuleRoutes } from './nodeModuleHandler.mjs'
|
|
3
|
+
import uws from 'uWebSockets.js'
|
|
4
|
+
import { createHandler, createNotFoundHandler } from './handler.mjs'
|
|
5
|
+
import { findRoutes } from './routes.mjs'
|
|
6
|
+
import path from 'path'
|
|
7
|
+
import fs from 'fs'
|
|
8
|
+
|
|
9
|
+
const state = {
|
|
10
|
+
routes: {},
|
|
11
|
+
initialized: false
|
|
12
|
+
}
|
|
13
|
+
const appMethods = {
|
|
14
|
+
GET: 'get',
|
|
15
|
+
POST: 'post',
|
|
16
|
+
PUT: 'put',
|
|
17
|
+
PATCH: 'patch',
|
|
18
|
+
DELETE: 'del',
|
|
19
|
+
OPTIONS: 'options',
|
|
20
|
+
HEAD: 'head',
|
|
21
|
+
CONNECT: 'connect',
|
|
22
|
+
TRACE: 'trace'
|
|
23
|
+
}
|
|
24
|
+
const optionsHandler = (config, methods) => {
|
|
25
|
+
return createHandler(() => ({
|
|
26
|
+
headers: {
|
|
27
|
+
allow: methods
|
|
28
|
+
},
|
|
29
|
+
statusCode: 204
|
|
30
|
+
}),
|
|
31
|
+
[],
|
|
32
|
+
[],
|
|
33
|
+
config
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const startHttpRedirect = (host, port) => {
|
|
38
|
+
// redirect http to https
|
|
39
|
+
uws.App().any('/*',
|
|
40
|
+
(req, res) => {
|
|
41
|
+
try {
|
|
42
|
+
res.writeHead(301, { Location: `https://${req.headers.host}:${port}${req.url}` })
|
|
43
|
+
res.end()
|
|
44
|
+
} catch (e) {
|
|
45
|
+
log.error(`Failed to handle http request on port ${port}`, req.url, e)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
).listen(host || '0.0.0.0', port, (token) => {
|
|
49
|
+
if (token) {
|
|
50
|
+
log.gne(`Http redirect server initialized at ${new Date().toISOString()} and listening on port ${port}`)
|
|
51
|
+
} else {
|
|
52
|
+
throw new Error(`Failed to start server on port ${port}`)
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const getHttpsApp = (key, cert) => {
|
|
58
|
+
const keyPath = path.resolve(key)
|
|
59
|
+
const certPath = path.resolve(cert)
|
|
60
|
+
if (!fs.existsSync(keyPath)) throw new Error(`Can't find https key file: ${keyPath}`)
|
|
61
|
+
if (!fs.existsSync(certPath)) throw new Error(`Can't find https cert file: ${keyPath}`)
|
|
62
|
+
return uws.App({
|
|
63
|
+
key_file_name: keyPath,
|
|
64
|
+
cert_file_name: certPath
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function startServer (config) {
|
|
69
|
+
if (!state.initialized) {
|
|
70
|
+
state.initialized = true
|
|
71
|
+
const routes = [...(await findRoutes(config)), ...getNodeModuleRoutes(config)]
|
|
72
|
+
let app, port
|
|
73
|
+
if (config.httpsKeyFile) {
|
|
74
|
+
app = getHttpsApp(config.secure)
|
|
75
|
+
port = config.secure.port || 14420
|
|
76
|
+
startHttpRedirect(config.host, config.port || 10420)
|
|
77
|
+
} else {
|
|
78
|
+
app = uws.App()
|
|
79
|
+
port = config.port || 10420
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const route of routes) {
|
|
83
|
+
if (config.printRoutes) {
|
|
84
|
+
log.info('Configured Route: ', route)
|
|
85
|
+
}
|
|
86
|
+
const routePattern = `^${route.urlPath.replace(/:[^/]+/g, '[^/]+').replace(/\*/g, '.*')}$`
|
|
87
|
+
if (config.notFoundRoute && config.notFoundRoute.match(routePattern)) {
|
|
88
|
+
config.notFoundRouteHandler = route
|
|
89
|
+
route.statusCodeOverride = 404
|
|
90
|
+
}
|
|
91
|
+
if (config.defaultRoute && config.defaultRoute.match(routePattern)) {
|
|
92
|
+
config.defaultRouteHandler = route
|
|
93
|
+
}
|
|
94
|
+
for (const method in route.handlers) {
|
|
95
|
+
const theHandler = createHandler(route.handlers[method], route.middleware, route.pathParameters, config)
|
|
96
|
+
app[appMethods[method]](route.urlPath, theHandler)
|
|
97
|
+
if (route.urlPath.endsWith('/') && route.urlPath.length > 1) {
|
|
98
|
+
app[appMethods[method]](route.urlPath.substr(0, route.urlPath.length - 1), theHandler)
|
|
99
|
+
}
|
|
100
|
+
if (route.urlPath.endsWith('/*')) {
|
|
101
|
+
app[appMethods[method]](route.urlPath.substr(0, route.urlPath.length - 2), theHandler)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (!route.handlers.OPTIONS) {
|
|
105
|
+
app.options(route.urlPath, optionsHandler(config, Object.keys(route.handlers).join(', ')))
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (config.notFoundRoute && !config.notFoundRouteHandler) {
|
|
110
|
+
log.warn('No route matched not found route: ' + config.notFoundRoute)
|
|
111
|
+
}
|
|
112
|
+
if (config.defaultRoute && !config.defaultRouteHandler) {
|
|
113
|
+
log.warn('No route matched default route: ' + config.notFoundRoute)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
app.any('/*', createNotFoundHandler(config))
|
|
117
|
+
app.listen(config.host || '0.0.0.0', config.port, (token) => {
|
|
118
|
+
if (token) {
|
|
119
|
+
log.gne(`Server initialized at ${new Date().toISOString()} and listening on port ${port}`)
|
|
120
|
+
} else {
|
|
121
|
+
throw new Error(`Failed to start server on port ${port}`)
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
return app
|
|
125
|
+
}
|
|
126
|
+
}
|