@srfnstack/spliffy 0.7.0 → 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.
@@ -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
+ }
package/src/index.mjs ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * A helper for creating a redirect handler
3
+ * @param location The location to redirect to
4
+ * @param permanent Whether this is a permanent redirect or not
5
+ */
6
+ export const redirect = (location, permanent = true) => () => ({
7
+ statusCode: permanent ? 301 : 302,
8
+ headers: {
9
+ location: location
10
+ }
11
+ })
12
+
13
+ export { default as log } from './log.mjs'
14
+ export { parseQuery, setMultiValueKey } from './url.mjs'
15
+
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
+ }