@jsenv/server 12.3.0 → 12.4.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/main.js CHANGED
@@ -42,6 +42,7 @@ export { pluginContentNegotiationCheck } from "./src/content_negotiation/plugin_
42
42
 
43
43
  // others
44
44
  export { fetchUrl } from "./src/fetch_url.js"
45
+ export { fromFetchResponse } from "./src/from_fetch_response.js"
45
46
  export { composeTwoResponses } from "./src/internal/response_composition.js"
46
47
  export { pluginRessourceAliases } from "./src/ressource_aliases/plugin_ressource_aliases.js"
47
48
  export { pluginRequestWaitingCheck } from "./src/plugin_request_waiting_check.js"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/server",
3
- "version": "12.3.0",
3
+ "version": "12.4.0",
4
4
  "description": "Write your Node.js server using pure functions",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -0,0 +1,15 @@
1
+ export const fromFetchResponse = (fetchResponse) => {
2
+ const responseHeaders = {}
3
+ const headersToIgnore = ["connection"]
4
+ fetchResponse.headers.forEach((value, name) => {
5
+ if (!headersToIgnore.includes(name)) {
6
+ responseHeaders[name] = value
7
+ }
8
+ })
9
+ return {
10
+ status: fetchResponse.status,
11
+ statusText: fetchResponse.statusText,
12
+ headers: responseHeaders,
13
+ body: fetchResponse.body, // node-fetch assumed
14
+ }
15
+ }
@@ -1,12 +1,17 @@
1
1
  // https://github.com/jamestalmage/stream-to-observable/blob/master/index.js
2
2
  import { Readable } from "node:stream"
3
-
4
3
  import { createObservable } from "./observable.js"
5
4
 
6
5
  const readableStreamLifetimeInSeconds = 120
7
6
 
8
7
  export const nodeStreamToObservable = (nodeStream) => {
9
8
  const observable = createObservable(({ next, error, complete }) => {
9
+ if (nodeStream.isPaused()) {
10
+ nodeStream.resume()
11
+ } else if (nodeStream.complete) {
12
+ complete()
13
+ return null
14
+ }
10
15
  const cleanup = () => {
11
16
  nodeStream.removeListener("data", next)
12
17
  nodeStream.removeListener("error", error)
@@ -14,13 +19,17 @@ export const nodeStreamToObservable = (nodeStream) => {
14
19
  nodeStream.removeListener("close", cleanup)
15
20
  nodeStream.destroy()
16
21
  }
17
-
18
22
  // should we do nodeStream.resume() in case the stream was paused ?
19
23
  nodeStream.once("error", error)
20
- nodeStream.on("data", next)
21
- nodeStream.once("close", cleanup)
22
- nodeStream.once("end", complete)
23
-
24
+ nodeStream.on("data", (data) => {
25
+ next(data)
26
+ })
27
+ nodeStream.once("close", () => {
28
+ cleanup()
29
+ })
30
+ nodeStream.once("end", () => {
31
+ complete()
32
+ })
24
33
  return cleanup
25
34
  })
26
35
 
@@ -1,3 +1,4 @@
1
+ import http from "node:http"
1
2
  import { Http2ServerResponse } from "node:http2"
2
3
  import { raceCallbacks } from "@jsenv/abort"
3
4
 
@@ -22,8 +23,8 @@ export const populateNodeResponse = async (
22
23
  status,
23
24
  statusText,
24
25
  headers,
26
+ onHeadersSent,
25
27
  })
26
- onHeadersSent()
27
28
 
28
29
  if (!body) {
29
30
  onEnd()
@@ -134,41 +135,48 @@ export const populateNodeResponse = async (
134
135
  })
135
136
  }
136
137
 
137
- const writeHead = (responseStream, { status, statusText, headers }) => {
138
+ const writeHead = (
139
+ responseStream,
140
+ { status, statusText, headers, onHeadersSent },
141
+ ) => {
138
142
  const responseIsHttp2ServerResponse =
139
143
  responseStream instanceof Http2ServerResponse
140
144
  const responseIsServerHttp2Stream =
141
145
  responseStream.constructor.name === "ServerHttp2Stream"
142
-
143
146
  let nodeHeaders = headersToNodeHeaders(headers, {
144
147
  // https://github.com/nodejs/node/blob/79296dc2d02c0b9872bbfcbb89148ea036a546d0/lib/internal/http2/compat.js#L112
145
148
  ignoreConnectionHeader:
146
149
  responseIsHttp2ServerResponse || responseIsServerHttp2Stream,
147
150
  })
148
-
151
+ if (statusText === undefined) {
152
+ statusText = statusTextFromStatus(status)
153
+ }
149
154
  if (responseIsServerHttp2Stream) {
150
155
  nodeHeaders = {
151
156
  ...nodeHeaders,
152
157
  ":status": status,
153
158
  }
154
159
  responseStream.respond(nodeHeaders)
160
+ onHeadersSent({ nodeHeaders, status, statusText })
155
161
  return
156
162
  }
157
-
158
163
  // nodejs strange signature for writeHead force this
159
164
  // https://nodejs.org/api/http.html#http_response_writehead_statuscode_statusmessage_headers
160
165
  if (
161
- statusText === undefined ||
162
166
  // https://github.com/nodejs/node/blob/79296dc2d02c0b9872bbfcbb89148ea036a546d0/lib/internal/http2/compat.js#L97
163
167
  responseIsHttp2ServerResponse
164
168
  ) {
165
169
  responseStream.writeHead(status, nodeHeaders)
170
+ onHeadersSent({ nodeHeaders, status, statusText })
166
171
  return
167
172
  }
168
-
169
173
  responseStream.writeHead(status, statusText, nodeHeaders)
174
+ onHeadersSent({ nodeHeaders, status, statusText })
170
175
  }
171
176
 
177
+ const statusTextFromStatus = (status) =>
178
+ http.STATUS_CODES[status] || "not specified"
179
+
172
180
  const headersToNodeHeaders = (headers, { ignoreConnectionHeader }) => {
173
181
  const nodeHeaders = {}
174
182
 
@@ -1,15 +1,21 @@
1
1
  import { urlToMeta, normalizeStructuredMetaMap } from "@jsenv/url-meta"
2
2
 
3
+ import { escapeRegexpSpecialCharacters } from "./regex_escape.js"
4
+
3
5
  export const pluginRessourceAliases = (ressourceAliases) => {
4
- const ressourceAliasMap = {}
6
+ const alias = {}
5
7
  Object.keys(ressourceAliases).forEach((key) => {
6
- ressourceAliasMap[ressourceAliases[key]] = key
8
+ alias[key] = {
9
+ pattern: key,
10
+ replacement: ressourceAliases[key],
11
+ }
7
12
  })
8
13
  const structuredMetaMap = normalizeStructuredMetaMap(
9
- { alias: ressourceAliasMap },
14
+ {
15
+ alias,
16
+ },
10
17
  "file:///",
11
18
  )
12
-
13
19
  const getRessourceAlias = (pathname) => {
14
20
  const { alias } = urlToMeta({
15
21
  url: `file://${pathname}`,
@@ -25,14 +31,35 @@ export const pluginRessourceAliases = (ressourceAliases) => {
25
31
  if (!ressourceAlias) {
26
32
  return
27
33
  }
28
-
29
- const urlObjectAfterAlias = replaceUrlRessource(
30
- request.url,
31
- ressourceAlias,
32
- )
33
- const ressource = `${urlObjectAfterAlias.pathname}${urlObjectAfterAlias.search}`
34
+ const { pattern, replacement } = ressourceAlias
35
+ let newValue
36
+ const ressourceBeforeAlias = request.ressource
37
+ if (replacement.includes("*")) {
38
+ let regexsource = ""
39
+ let i = 0
40
+ while (i < pattern.length) {
41
+ const char = pattern[i]
42
+ i++
43
+ if (char === "*") {
44
+ regexsource += "(.+)"
45
+ } else {
46
+ regexsource += escapeRegexpSpecialCharacters(char)
47
+ }
48
+ }
49
+ const regex = new RegExp(regexsource)
50
+ const matchResult = ressourceBeforeAlias.match(regex)
51
+ const parts = replacement.split("*")
52
+ newValue = parts.reduce((previous, value, index) => {
53
+ return `${previous}${value}${
54
+ index === parts.length - 1 ? "" : matchResult[index + 1]
55
+ }`
56
+ }, "")
57
+ } else {
58
+ newValue = replacement
59
+ }
60
+ const ressource = replaceRessource(ressourceBeforeAlias, newValue)
34
61
  redirectRequest({
35
- ressourceBeforeAlias: request.ressource,
62
+ ressourceBeforeAlias,
36
63
  ressource,
37
64
  })
38
65
  },
@@ -40,16 +67,13 @@ export const pluginRessourceAliases = (ressourceAliases) => {
40
67
  }
41
68
  }
42
69
 
43
- const replaceUrlRessource = (url, ressource) => {
44
- const urlObject = new URL(url)
45
-
46
- const searchSeparatorIndex = ressource.indexOf("?")
47
- if (searchSeparatorIndex === -1) {
48
- urlObject.pathname = ressource
49
- return urlObject
70
+ const replaceRessource = (ressourceBeforeAlias, newValue) => {
71
+ const urlObject = new URL(ressourceBeforeAlias, "file://")
72
+ const searchSeparatorIndex = newValue.indexOf("?")
73
+ if (searchSeparatorIndex > -1) {
74
+ return newValue // let new value override search params
50
75
  }
51
-
52
- const pathname = ressource.slice(0, searchSeparatorIndex)
53
- urlObject.pathname = pathname
54
- return urlObject
76
+ urlObject.pathname = newValue
77
+ const ressource = `${urlObject.pathname}${urlObject.search}`
78
+ return ressource
55
79
  }
@@ -0,0 +1,34 @@
1
+ // https://github.com/benjamingr/RegExp.escape/blob/master/polyfill.js
2
+
3
+ export const escapeRegexpSpecialCharacters = (string) => {
4
+ string = String(string)
5
+ let i = 0
6
+ let escapedString = ""
7
+
8
+ while (i < string.length) {
9
+ const char = string[i]
10
+ i++
11
+ escapedString += isRegExpSpecialChar(char) ? `\\${char}` : char
12
+ }
13
+ return escapedString
14
+ }
15
+
16
+ const isRegExpSpecialChar = (char) => regexpSpecialChars.indexOf(char) > -1
17
+
18
+ const regexpSpecialChars = [
19
+ "/",
20
+ "^",
21
+ "\\",
22
+ "[",
23
+ "]",
24
+ "(",
25
+ ")",
26
+ "{",
27
+ "}",
28
+ "?",
29
+ "+",
30
+ "*",
31
+ ".",
32
+ "|",
33
+ "$",
34
+ ]
@@ -230,6 +230,10 @@ export const startServer = async ({
230
230
  })
231
231
 
232
232
  const requestCallback = async (nodeRequest, nodeResponse) => {
233
+ // pause the stream to let a chance to "requestToResponse"
234
+ // to call "requestRequestBody". Without this the request body readable stream
235
+ // might be closed when we'll try to attach "data" and "end" listeners to it
236
+ nodeRequest.pause()
233
237
  if (!nagle) {
234
238
  nodeRequest.connection.setNoDelay(true)
235
239
  }
@@ -309,7 +313,6 @@ export const startServer = async ({
309
313
 
310
314
  visitRequestNodeToLog(rootRequestNode, 0)
311
315
  }
312
-
313
316
  nodeRequest.on("error", (error) => {
314
317
  if (error.message === "aborted") {
315
318
  addRequestLog(rootRequestNode, {
@@ -570,7 +573,7 @@ export const startServer = async ({
570
573
  return composeTwoResponses(
571
574
  {
572
575
  status: 500,
573
- statusText: statusToStatusText(500),
576
+ statusText: "Internal Server Error",
574
577
  headers: {
575
578
  // ensure error are not cached
576
579
  "cache-control": "no-store",
@@ -580,9 +583,21 @@ export const startServer = async ({
580
583
  propertiesFromError,
581
584
  )
582
585
  }
583
- const responseProperties = normalizeResponseProperties(
584
- result.returnValue,
585
- )
586
+
587
+ const {
588
+ status = 501,
589
+ statusText,
590
+ headers = {},
591
+ body,
592
+ ...rest
593
+ } = result.returnValue || {}
594
+ const responseProperties = {
595
+ status,
596
+ statusText,
597
+ headers,
598
+ body,
599
+ ...rest,
600
+ }
586
601
  return responseProperties
587
602
  }
588
603
 
@@ -652,12 +667,10 @@ export const startServer = async ({
652
667
  })
653
668
  onRequestHandled(requestNode)
654
669
  },
655
- onHeadersSent: () => {
670
+ onHeadersSent: ({ status, statusText }) => {
656
671
  addRequestLog(requestNode, {
657
672
  type: "info",
658
- value: `${colorizeResponseStatus(responseProperties.status)} ${
659
- responseProperties.statusText
660
- }`,
673
+ value: `${colorizeResponseStatus(status)} ${statusText}`,
661
674
  })
662
675
  },
663
676
  onEnd: () => {
@@ -673,6 +686,7 @@ export const startServer = async ({
673
686
  const responseProperties = await handleRequest(request, {
674
687
  requestNode: rootRequestNode,
675
688
  })
689
+ nodeRequest.resume()
676
690
  if (receiveRequestOperation.signal.aborted) {
677
691
  return
678
692
  }
@@ -742,38 +756,6 @@ const createNodeServer = async ({
742
756
  })
743
757
  }
744
758
 
745
- const normalizeResponseProperties = (returnValue) => {
746
- if (returnValue && returnValue.constructor.name === "Response") {
747
- const responseHeaders = {}
748
- const headersToIgnore = ["connection"]
749
- returnValue.headers.forEach((value, name) => {
750
- if (!headersToIgnore.includes(name)) {
751
- responseHeaders[name] = value
752
- }
753
- })
754
- return {
755
- status: returnValue.status,
756
- statusText: returnValue.statusText,
757
- headers: responseHeaders,
758
- body: returnValue.body, // node-fetch assumed
759
- }
760
- }
761
- const {
762
- status = 501,
763
- statusText = statusToStatusText(status),
764
- headers = {},
765
- body,
766
- ...rest
767
- } = returnValue || {}
768
- return {
769
- status,
770
- statusText,
771
- headers,
772
- body,
773
- ...rest,
774
- }
775
- }
776
-
777
759
  const generateResponseDescription = async ({
778
760
  requestToResponse,
779
761
  request,
@@ -797,10 +779,6 @@ const generateResponseDescription = async ({
797
779
  }
798
780
  }
799
781
  }
800
-
801
- const statusToStatusText = (status) =>
802
- http.STATUS_CODES[status] || "not specified"
803
-
804
782
  const testCanPushStream = (http2Stream) => {
805
783
  if (!http2Stream.pushAllowed) {
806
784
  return {