@srfnstack/spliffy 0.8.1 → 0.9.1

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/package.json CHANGED
@@ -1,16 +1,17 @@
1
1
  {
2
2
  "name": "@srfnstack/spliffy",
3
- "version": "0.8.1",
3
+ "version": "0.9.1",
4
4
  "author": "snowbldr",
5
5
  "private": false,
6
6
  "homepage": "https://github.com/narcolepticsnowman/spliffy",
7
7
  "license": "MIT",
8
+ "type": "module",
8
9
  "files": [
9
10
  "src/*",
10
11
  "LICENSE.txt",
11
12
  "README.md"
12
13
  ],
13
- "main": "src/index.js",
14
+ "main": "src/index.mjs",
14
15
  "repository": {
15
16
  "type": "git",
16
17
  "url": "git@github.com:narcolepticsnowman/spliffy.git"
package/src/content.mjs CHANGED
@@ -3,6 +3,7 @@ import { parseQuery } from './url.mjs'
3
3
 
4
4
  const defaultHandler = {
5
5
  deserialize: o => {
6
+ if (!o) return o
6
7
  try {
7
8
  return JSON.parse(o && o.toString())
8
9
  } catch (e) {
@@ -10,6 +11,7 @@ const defaultHandler = {
10
11
  }
11
12
  },
12
13
  serialize: o => {
14
+ if (!o) return { data: o }
13
15
  if (typeof o === 'string') {
14
16
  return {
15
17
  contentType: 'text/plain',
@@ -41,8 +43,8 @@ const toFormData = (key, value) => {
41
43
 
42
44
  const contentHandlers = {
43
45
  'application/json': {
44
- deserialize: s => JSON.parse(s && s.toString()),
45
- serialize: o => JSON.stringify(o)
46
+ deserialize: s => s && JSON.parse(s && s.toString()),
47
+ serialize: o => o && JSON.stringify(o)
46
48
  },
47
49
  'text/plain': {
48
50
  deserialize: s => s && s.toString(),
@@ -51,7 +53,7 @@ const contentHandlers = {
51
53
  'application/octet-stream': defaultHandler,
52
54
  'application/x-www-form-urlencoded': {
53
55
  deserialize: s => s && parseQuery(s.toString(), true),
54
- serialize: o => Object.keys(o).map(toFormData).flat().join('&')
56
+ serialize: o => o && Object.keys(o).map(toFormData).flat().join('&')
55
57
  },
56
58
  '*/*': defaultHandler
57
59
  }
package/src/decorator.mjs CHANGED
@@ -1,10 +1,11 @@
1
1
  import cookie from 'cookie'
2
2
  import http from 'http'
3
- import { parseQuery, setMultiValueKey } from './url.mjs'
3
+ import { parseQuery } from './url.mjs'
4
4
  import log from './log.mjs'
5
5
  import { v4 as uuid } from 'uuid'
6
6
  import stream from 'stream'
7
7
  import httpStatusCodes, { defaultStatusMessages } from './httpStatusCodes.mjs'
8
+
8
9
  const { Writable } = stream
9
10
 
10
11
  const addressArrayBufferToString = addrBuf => String.fromCharCode.apply(null, new Int8Array(addrBuf))
@@ -26,42 +27,54 @@ export const setCookie = (res) => function () {
26
27
  return res.setHeader('Set-Cookie', [...(res.getHeader('Set-Cookie') || []), cookie.serialize(...arguments)])
27
28
  }
28
29
 
29
- export function decorateRequest (uwsReq, pathParameters, res, { decodeQueryParameters, decodePathParameters, parseCookie } = {}) {
30
+ export function decorateRequest (uwsReq, pathParameters, res, {
31
+ decodeQueryParameters,
32
+ decodePathParameters,
33
+ parseCookie,
34
+ extendIncomingMessage
35
+ } = {}) {
30
36
  // uwsReq can't be used in async functions because it gets de-allocated when the handler function returns
31
37
  const req = {}
32
- // frameworks like passport like to modify the message prototype
33
- // Setting the prototype of req is not desirable because the entire api of IncomingMessage is not supported
34
- for (const p of reqProtoProps()) {
35
- if (!req[p]) req[p] = http.IncomingMessage.prototype[p]
38
+ if (extendIncomingMessage) {
39
+ // frameworks like passport like to modify the message prototype
40
+ // Setting the prototype of req is not desirable because the entire api of IncomingMessage is not supported
41
+ for (const p of reqProtoProps()) {
42
+ if (!req[p]) req[p] = http.IncomingMessage.prototype[p]
43
+ }
36
44
  }
37
45
  const query = uwsReq.getQuery()
38
46
  req.path = uwsReq.getUrl()
39
47
  req.url = `${req.path}${query ? '?' + query : ''}`
40
48
  req.spliffyUrl = {
41
- path: uwsReq.getUrl(),
42
- query: parseQuery(uwsReq.getQuery(), decodeQueryParameters)
49
+ path: req.path,
50
+ query: query && parseQuery(query, decodeQueryParameters),
51
+ pathParameters: {}
43
52
  }
44
- req.spliffyUrl.pathParameters = {}
45
53
  if (pathParameters && pathParameters.length > 0) {
46
54
  for (const i in pathParameters) {
47
- req.spliffyUrl.pathParameters[pathParameters[i]] = decodePathParameters
48
- ? decodeURIComponent(uwsReq.getParameter(i))
49
- : uwsReq.getParameter(i)
55
+ req.spliffyUrl.pathParameters[pathParameters[i]] =
56
+ decodePathParameters
57
+ ? decodeURIComponent(uwsReq.getParameter(i))
58
+ : uwsReq.getParameter(i)
50
59
  }
51
60
  }
52
61
  req.params = req.spliffyUrl.pathParameters
53
62
  req.headers = {}
63
+ uwsReq.forEach((header, value) => { req.headers[header] = value })
54
64
  req.method = uwsReq.getMethod().toUpperCase()
55
65
  req.remoteAddress = addressArrayBufferToString(res.getRemoteAddressAsText())
56
66
  req.proxiedRemoteAddress = addressArrayBufferToString(res.getProxiedRemoteAddressAsText())
57
- uwsReq.forEach((header, value) => setMultiValueKey(req.headers, normalizeHeader(header), value))
58
- req.get = header => req.headers[normalizeHeader(header)]
67
+ req.get = header => req.headers[header]
59
68
  if (parseCookie && req.headers.cookie) {
60
69
  req.cookies = cookie.parse(req.headers.cookie) || {}
61
70
  }
62
71
  return req
63
72
  }
64
73
 
74
+ function toArrayBuffer (buffer) {
75
+ return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)
76
+ }
77
+
65
78
  export function decorateResponse (res, req, finalizeResponse, errorTransformer, endError, { acceptsDefault }) {
66
79
  res.onAborted(() => {
67
80
  res.ended = true
@@ -114,31 +127,37 @@ export function decorateResponse (res, req, finalizeResponse, errorTransformer,
114
127
  return this
115
128
  }
116
129
 
117
- function toArrayBuffer (buffer) {
118
- return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)
130
+ res.writeArrayBuffer = res.write
131
+ res.write = (chunk, encoding, cb) => {
132
+ try {
133
+ res.streaming = true
134
+ res.flushHeaders()
135
+ let result
136
+ if (chunk instanceof Buffer) {
137
+ result = res.writeArrayBuffer(toArrayBuffer(chunk))
138
+ } else if (typeof chunk === 'string') {
139
+ result = res.writeArrayBuffer(toArrayBuffer(Buffer.from(chunk, encoding || 'utf8')))
140
+ } else {
141
+ result = res.writeArrayBuffer(toArrayBuffer(Buffer.from(JSON.stringify(chunk), encoding || 'utf8')))
142
+ }
143
+ if (typeof cb === 'function') {
144
+ cb()
145
+ }
146
+ return result
147
+ } catch (e) {
148
+ if (typeof cb === 'function') {
149
+ cb(e)
150
+ } else {
151
+ throw e
152
+ }
153
+ }
119
154
  }
120
-
121
155
  let outStream
122
156
  res.getWritable = () => {
123
157
  if (!outStream) {
124
158
  res.streaming = true
125
159
  outStream = new Writable({
126
- write: (chunk, encoding, cb) => {
127
- try {
128
- res.flushHeaders()
129
- const result = res.write(chunk)
130
- if (typeof cb === 'function') {
131
- cb()
132
- }
133
- return result
134
- } catch (e) {
135
- if (typeof cb === 'function') {
136
- cb(e)
137
- } else {
138
- throw e
139
- }
140
- }
141
- }
160
+ write: res.write
142
161
  })
143
162
  .on('finish', res.end)
144
163
  .on('end', res.end)
@@ -152,18 +171,6 @@ export function decorateResponse (res, req, finalizeResponse, errorTransformer,
152
171
  }
153
172
  return outStream
154
173
  }
155
- res.writeArrayBuffer = res.write
156
- res.write = chunk => {
157
- res.streaming = true
158
- res.flushHeaders()
159
- if (chunk instanceof Buffer) {
160
- return res.writeArrayBuffer(toArrayBuffer(chunk))
161
- } else if (typeof chunk === 'string') {
162
- return res.writeArrayBuffer(toArrayBuffer(Buffer.from(chunk, 'utf8')))
163
- } else {
164
- return res.writeArrayBuffer(toArrayBuffer(Buffer.from(JSON.stringify(chunk), 'utf8')))
165
- }
166
- }
167
174
 
168
175
  const uwsEnd = res.end
169
176
  res.ended = false
package/src/handler.mjs CHANGED
@@ -11,14 +11,18 @@ const { Readable } = stream
11
11
  * @param url The url being requested
12
12
  * @param res The uws response object
13
13
  * @param req The uws request object
14
- * @param body The request body
14
+ * @param bodyPromise The request body promise
15
15
  * @param handler The handler function for the route
16
16
  * @param middleware The middleware that applies to this request
17
17
  * @param errorTransformer An errorTransformer to convert error objects into response data
18
18
  */
19
- const executeHandler = async (url, res, req, body, handler, middleware, errorTransformer) => {
19
+ const executeHandler = async (url, res, req, bodyPromise, handler, middleware, errorTransformer) => {
20
20
  try {
21
- if (body) body = deserializeBody(body, req.headers['content-type'], res.acceptsDefault)
21
+ bodyPromise = bodyPromise.then(bodyContent => {
22
+ if (bodyContent instanceof Readable) return bodyContent
23
+ if (res.writableEnded) return
24
+ return deserializeBody(bodyContent, req.headers['content-type'], res.acceptsDefault)
25
+ })
22
26
  } catch (e) {
23
27
  log.error('Failed to parse request.', e)
24
28
  end(res, 400, handler.statusCodeOverride)
@@ -26,7 +30,7 @@ const executeHandler = async (url, res, req, body, handler, middleware, errorTra
26
30
  }
27
31
 
28
32
  try {
29
- const handled = await handler({ url, body, headers: req.headers, req, res })
33
+ const handled = await handler({ url, bodyPromise, headers: req.headers, req, res })
30
34
  finalizeResponse(req, res, handled, handler.statusCodeOverride)
31
35
  } catch (e) {
32
36
  const refId = uuid()
@@ -103,15 +107,10 @@ const pipeResponse = (res, readStream, errorTransformer) => {
103
107
  }
104
108
 
105
109
  const doSerializeBody = (body, res) => {
106
- const contentType = res.getHeader('content-type')
107
- if (typeof body === 'string') {
108
- if (!contentType) {
109
- res.headers['content-type'] = 'text/plain'
110
- }
111
- return body
112
- } else if (body instanceof Readable) {
110
+ if (!body || typeof body === 'string' || body instanceof Readable) {
113
111
  return body
114
112
  }
113
+ const contentType = res.getHeader('content-type')
115
114
  const serialized = serializeBody(body, contentType, res.acceptsDefault)
116
115
 
117
116
  if (serialized?.contentType && !contentType) {
@@ -139,17 +138,12 @@ async function executeMiddleware (middleware, req, res, errorTransformer, refId,
139
138
  }
140
139
  }
141
140
 
142
- let currentDate = new Date().toUTCString()
143
- setInterval(() => { currentDate = new Date().toUTCString() }, 1000)
144
-
145
141
  const handleRequest = async (req, res, handler, middleware, errorTransformer) => {
146
- res.headers.date = currentDate
147
-
148
142
  try {
149
143
  let reqBody
150
144
  if (!handler.streamRequestBody) {
151
145
  let buffer
152
- reqBody = await new Promise(
146
+ reqBody = new Promise(
153
147
  resolve =>
154
148
  res.onData(async (data, isLast) => {
155
149
  if (isLast) {
@@ -160,18 +154,19 @@ const handleRequest = async (req, res, handler, middleware, errorTransformer) =>
160
154
  })
161
155
  )
162
156
  } else {
163
- reqBody = new Readable({
157
+ const readable = new Readable({
164
158
  read: () => {
165
159
  }
166
160
  })
167
161
  res.onData(async (data, isLast) => {
168
162
  if (data.byteLength === 0 && !isLast) return
169
163
  // data must be copied so it isn't lost
170
- reqBody.push(Buffer.concat([Buffer.from(data)]))
164
+ readable.push(Buffer.concat([Buffer.from(data)]))
171
165
  if (isLast) {
172
- reqBody.push(null)
166
+ readable.push(null)
173
167
  }
174
168
  })
169
+ reqBody = Promise.resolve(readable)
175
170
  }
176
171
  await executeMiddleware(middleware, req, res, errorTransformer)
177
172
  if (!res.writableEnded && !res.ended) {
@@ -187,6 +182,9 @@ const handleRequest = async (req, res, handler, middleware, errorTransformer) =>
187
182
 
188
183
  export const HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD', 'CONNECT', 'TRACE']
189
184
 
185
+ let currentDate = new Date().toISOString()
186
+ setInterval(() => { currentDate = new Date().toISOString() }, 1000)
187
+
190
188
  export const createHandler = (handler, middleware, pathParameters, config) => function (res, req) {
191
189
  try {
192
190
  req = decorateRequest(req, pathParameters, res, config)
@@ -195,10 +193,21 @@ export const createHandler = (handler, middleware, pathParameters, config) => fu
195
193
  if (config.logAccess) {
196
194
  res.onEnd = writeAccess(req, res)
197
195
  }
196
+
197
+ if (config.writeDateHeader) {
198
+ res.headers.date = currentDate
199
+ }
200
+
198
201
  handleRequest(req, res, handler, middleware, config.errorTransformer)
199
- .catch(e => log.error('Failed handling request', e))
202
+ .catch(e => {
203
+ log.error('Failed handling request', e)
204
+ res.statusCode = 500
205
+ res.end()
206
+ })
200
207
  } catch (e) {
201
208
  log.error('Failed handling request', e)
209
+ res.statusCode = 500
210
+ res.end()
202
211
  }
203
212
  }
204
213
 
@@ -37,9 +37,7 @@ export const cloneMiddleware = (middleware) => {
37
37
  * @param middleware
38
38
  */
39
39
  export const validateMiddleware = (middleware) => {
40
- if (Array.isArray(middleware)) {
41
- validateMiddlewareArray(middleware)
42
- } else if (typeof middleware === 'object') {
40
+ if (!Array.isArray(middleware) && typeof middleware === 'object') {
43
41
  for (const method in middleware) {
44
42
  // ensure methods are always available as uppercase
45
43
  const upMethod = method.toUpperCase()
@@ -47,7 +45,7 @@ export const validateMiddleware = (middleware) => {
47
45
  validateMiddlewareArray(middleware[upMethod])
48
46
  }
49
47
  } else {
50
- throw new Error('Invalid middleware definition: ' + middleware)
48
+ validateMiddlewareArray(middleware)
51
49
  }
52
50
  }
53
51
 
package/src/routes.mjs CHANGED
@@ -27,12 +27,16 @@ const getPathPart = name => {
27
27
  const filterTestFiles = config => f => (!f.name.endsWith('.test.js') && !f.name.endsWith('.test.mjs')) || config.allowTestFileRoutes
28
28
  const filterIgnoredFiles = config => f => !config.ignoreFilesMatching.filter(p => p).find(pattern => f.name.match(pattern))
29
29
  const ignoreHandlerFields = { middleware: true, streamRequestBody: true }
30
+
31
+ const isRouteFile = name => name.endsWith('.rt.js') || name.endsWith('.rt.mjs') || name.endsWith('.rt.cjs')
32
+ const isMiddlewareFile = name => name.endsWith('.mw.js') || name.endsWith('.mw.mjs') || name.endsWith('.mw.cjs')
33
+
30
34
  const doFindRoutes = async (config, currentFile, filePath, urlPath, pathParameters, inheritedMiddleware) => {
31
35
  const routes = []
32
36
  const name = currentFile.name
33
37
  if (currentFile.isDirectory()) {
34
38
  routes.push(...(await findRoutesInDir(name, filePath, urlPath, inheritedMiddleware, pathParameters, config)))
35
- } else if (!config.staticMode && (name.endsWith('.rt.js') || name.endsWith('.rt.mjs'))) {
39
+ } else if (!config.staticMode && isRouteFile(name)) {
36
40
  routes.push(await buildJSHandlerRoute(name, filePath, urlPath, inheritedMiddleware, pathParameters))
37
41
  } else {
38
42
  routes.push(...buildStaticRoutes(name, filePath, urlPath, inheritedMiddleware, pathParameters, config))
@@ -47,6 +51,14 @@ const importModules = async (config, dirPath, files) => Promise.all(
47
51
  .map(f => path.join(dirPath, f.name))
48
52
  .map(mwPath => import(`file://${mwPath}`)
49
53
  .then(module => ({ module, mwPath }))
54
+ .catch(e => {
55
+ // Hack to workaround https://github.com/nodejs/modules/issues/471
56
+ if (e instanceof SyntaxError) {
57
+ const newError = new SyntaxError(`${e.message}. In file: ${mwPath}`)
58
+ newError.stack = e.stack
59
+ throw newError
60
+ }
61
+ })
50
62
  ))
51
63
 
52
64
  const findRoutesInDir = async (name, filePath, urlPath, inheritedMiddleware, pathParameters, config) => {
@@ -55,9 +67,7 @@ const findRoutesInDir = async (name, filePath, urlPath, inheritedMiddleware, pat
55
67
  }
56
68
  const files = await readdir(filePath, { withFileTypes: true })
57
69
 
58
- const middlewareModules = await importModules(config, filePath,
59
- files.filter(f => f.name.endsWith('.mw.js') || f.name.endsWith('.mw.mjs'))
60
- )
70
+ const middlewareModules = await importModules(config, filePath, files.filter(f => isMiddlewareFile(f.name)))
61
71
  const dirMiddleware = middlewareModules.map(({ module, mwPath }) => {
62
72
  const middleware = module.middleware || module.default?.middleware
63
73
  if (!middleware) {
@@ -73,7 +83,7 @@ const findRoutesInDir = async (name, filePath, urlPath, inheritedMiddleware, pat
73
83
  .reduce((result, incoming) => mergeMiddleware(incoming, result), inheritedMiddleware)
74
84
 
75
85
  return Promise.all(files
76
- .filter(f => !f.name.endsWith('.mw.js') && !f.name.endsWith('.mw.mjs'))
86
+ .filter(f => !isMiddlewareFile(f.name))
77
87
  .filter(filterTestFiles(config))
78
88
  .filter(filterIgnoredFiles(config))
79
89
  .map(
@@ -90,7 +100,7 @@ const findRoutesInDir = async (name, filePath, urlPath, inheritedMiddleware, pat
90
100
  }
91
101
 
92
102
  const buildJSHandlerRoute = async (name, filePath, urlPath, inheritedMiddleware, pathParameters) => {
93
- if (name.endsWith('.mjs')) {
103
+ if (name.endsWith('.mjs') || name.endsWith('.cjs')) {
94
104
  name = name.substr(0, name.length - '.rt.mjs'.length)
95
105
  } else {
96
106
  name = name.substr(0, name.length - '.rt.js'.length)
@@ -104,10 +114,25 @@ const buildJSHandlerRoute = async (name, filePath, urlPath, inheritedMiddleware,
104
114
  filePath,
105
115
  handlers: {}
106
116
  }
107
- const module = await import(`file://${filePath}`)
117
+ let module
118
+ try {
119
+ module = await import(`file://${filePath}`)
120
+ } catch (e) {
121
+ // Hack to workaround https://github.com/nodejs/modules/issues/471
122
+ if (e instanceof SyntaxError) {
123
+ const newError = new SyntaxError(`${e.message}. In file: ${filePath}`)
124
+ newError.stack = e.stack
125
+ throw newError
126
+ }
127
+ }
108
128
  const handlers = module.default
109
-
110
- route.middleware = mergeMiddleware(handlers.middleware || [], inheritedMiddleware)
129
+ try {
130
+ route.middleware = mergeMiddleware(handlers.middleware || [], inheritedMiddleware)
131
+ } catch (e) {
132
+ const err = new Error(`Failed to load middleware for route: ${filePath}`)
133
+ err.stack += `\nCaused By: ${e.stack}`
134
+ throw err
135
+ }
111
136
  for (const method of Object.keys(handlers).filter(k => !ignoreHandlerFields[k])) {
112
137
  if (HTTP_METHODS.indexOf(method) === -1) {
113
138
  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.`)
package/src/server.mjs CHANGED
@@ -21,14 +21,14 @@ const appMethods = {
21
21
  CONNECT: 'connect',
22
22
  TRACE: 'trace'
23
23
  }
24
- const optionsHandler = (config, methods) => {
24
+ const optionsHandler = (config, middleware, methods) => {
25
25
  return createHandler(() => ({
26
26
  headers: {
27
27
  allow: methods
28
28
  },
29
29
  statusCode: 204
30
30
  }),
31
- [],
31
+ middleware,
32
32
  [],
33
33
  config
34
34
  )
@@ -102,7 +102,7 @@ export async function startServer (config) {
102
102
  }
103
103
  }
104
104
  if (!route.handlers.OPTIONS) {
105
- app.options(route.urlPath, optionsHandler(config, Object.keys(route.handlers).join(', ')))
105
+ app.options(route.urlPath, optionsHandler(config, route.middleware, Object.keys(route.handlers).join(', ')))
106
106
  }
107
107
  }
108
108
 
@@ -27,11 +27,23 @@ export async function initConfig (userConfig) {
27
27
  const config = Object.assign({}, userConfig)
28
28
 
29
29
  if (!('decodePathParameters' in config)) {
30
- config.decodePathParameters = true
30
+ config.decodePathParameters = false
31
+ }
32
+
33
+ if (!('decodeQueryParams' in config)) {
34
+ config.decodeQueryParams = false
35
+ }
36
+
37
+ if (!('extendIncomingMessage' in config)) {
38
+ config.extendIncomingMessage = false
31
39
  }
32
40
 
33
41
  if (!('parseCookie' in config)) {
34
- config.parseCookie = true
42
+ config.parseCookie = false
43
+ }
44
+
45
+ if (!('writeDateHeader' in config)) {
46
+ config.writeDateHeader = false
35
47
  }
36
48
 
37
49
  config.acceptsDefault = config.acceptsDefault || defaultHeaders.acceptsDefault
@@ -55,7 +67,7 @@ export async function initConfig (userConfig) {
55
67
  }
56
68
 
57
69
  if (!('logAccess' in config)) {
58
- config.logAccess = true
70
+ config.logAccess = false
59
71
  }
60
72
  if ('logLevel' in config) {
61
73
  log.setLogLevel(config.logLevel)