@jsenv/server 10.1.6 → 12.0.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/README.md CHANGED
@@ -70,7 +70,10 @@ const notFoundService = (request) => {
70
70
  }
71
71
 
72
72
  const server = await startServer({
73
- requestToResponse: composeServices(indexService, notFoundService),
73
+ requestToResponse: composeServices({
74
+ index: indexService,
75
+ otherwise: notFoundService,
76
+ }),
74
77
  })
75
78
 
76
79
  const fetch = await import("node-fetch")
package/main.js CHANGED
@@ -5,11 +5,8 @@
5
5
  */
6
6
 
7
7
  export { startServer } from "./src/startServer.js"
8
- export {
9
- composeServices,
10
- composeServicesWithTiming,
11
- redirectRequest,
12
- } from "./src/service_composition/service_composition.js"
8
+ export { composeServices } from "./src/service_composition/service_composition.js"
9
+ export { setupRoutes } from "./src/service_composition/routing.js"
13
10
  export { jsenvServerInternalErrorToResponse } from "./src/jsenvServerInternalErrorToResponse.js"
14
11
  export { readRequestBody } from "./src/readRequestBody.js"
15
12
  export { fetchFileSystem } from "./src/fetch_filesystem.js"
@@ -46,6 +43,7 @@ export { pluginContentNegotiationCheck } from "./src/content_negotiation/plugin_
46
43
  // others
47
44
  export { fetchUrl } from "./src/fetch_url.js"
48
45
  export { composeTwoResponses } from "./src/internal/response_composition.js"
46
+ export { pluginRessourceAliases } from "./src/ressource_aliases/plugin_ressource_aliases.js"
49
47
  export { pluginRequestWaitingCheck } from "./src/plugin_request_waiting_check.js"
50
48
  export { findFreePort } from "./src/internal/listen.js"
51
49
  export { urlToContentType } from "./src/urlToContentType.js"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/server",
3
- "version": "10.1.6",
3
+ "version": "12.0.0",
4
4
  "description": "Write your Node.js server using pure functions",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -13,7 +13,7 @@
13
13
  "url": "https://github.com/jsenv/server"
14
14
  },
15
15
  "engines": {
16
- "node": ">=14.17.0"
16
+ "node": ">=16.12.0"
17
17
  },
18
18
  "publishConfig": {
19
19
  "access": "public"
@@ -25,13 +25,14 @@
25
25
  },
26
26
  "./*": "./*"
27
27
  },
28
+ "main": "./main.js",
28
29
  "files": [
29
30
  "/src/",
30
31
  "/main.js"
31
32
  ],
32
33
  "scripts": {
33
34
  "eslint-check": "node ./node_modules/eslint/bin/eslint.js . --ext=.js,.mjs",
34
- "generate-importmap": "node ./script/importmap/generate_importmap.mjs",
35
+ "importmap-generate": "node ./script/importmap/generate_importmap.mjs",
35
36
  "measure-performances": "node --expose-gc ./script/performance/generate_performance_report.mjs --log",
36
37
  "test": "node --unhandled-rejections=strict ./script/test/test.mjs",
37
38
  "test-with-coverage": "npm run test -- --coverage",
@@ -47,28 +48,27 @@
47
48
  "dependencies": {
48
49
  "@jsenv/abort": "4.1.1",
49
50
  "@jsenv/logger": "4.0.1",
50
- "node-fetch": "2.6.2"
51
+ "@jsenv/url-meta": "6.0.1",
52
+ "node-fetch": "2.6.2",
53
+ "path-to-regexp": "6.2.0"
51
54
  },
52
55
  "devDependencies": {
53
- "@babel/core": "7.16.0",
54
- "@babel/eslint-parser": "7.16.0",
55
56
  "@jsenv/assert": "2.3.2",
56
- "@jsenv/codecov-upload": "3.5.0",
57
- "@jsenv/core": "23.7.1",
58
- "@jsenv/eslint-config": "16.0.8",
57
+ "@jsenv/core": "23.10.0",
58
+ "@jsenv/eslint-config": "16.0.9",
59
59
  "@jsenv/filesystem": "2.5.1",
60
60
  "@jsenv/github-release-package": "1.2.3",
61
- "@jsenv/https-local": "1.0.2",
61
+ "@jsenv/https-local": "1.0.3",
62
62
  "@jsenv/importmap-eslint-resolver": "5.1.2",
63
- "@jsenv/importmap-node-module": "2.4.1",
63
+ "@jsenv/importmap-node-module": "2.7.1",
64
64
  "@jsenv/package-publish": "1.6.2",
65
65
  "@jsenv/performance-impact": "2.2.1",
66
66
  "@jsenv/prettier-check-project": "5.6.1",
67
- "eslint": "7.32.0",
68
- "eslint-plugin-import": "2.25.2",
67
+ "eslint": "8.2.0",
68
+ "eslint-plugin-import": "2.25.3",
69
69
  "eventsource": "1.1.0",
70
- "fastify": "3.22.1",
71
- "fastify-static": "4.4.2",
70
+ "fastify": "3.24.0",
71
+ "fastify-static": "4.5.0",
72
72
  "playwright": "1.16.3",
73
73
  "prettier": "2.4.1"
74
74
  }
@@ -1,7 +1,7 @@
1
1
  import { nodeStreamToObservable } from "./nodeStreamToObservable.js"
2
2
  import { headersFromObject } from "./headersFromObject.js"
3
3
 
4
- export const nodeRequestToRequest = (nodeRequest, { signal, serverOrigin }) => {
4
+ export const fromNodeRequest = (nodeRequest, { serverOrigin, signal }) => {
5
5
  const headers = headersFromObject(nodeRequest.headers)
6
6
  const body = nodeStreamToObservable(nodeRequest)
7
7
 
@@ -18,67 +18,88 @@ export const nodeRequestToRequest = (nodeRequest, { signal, serverOrigin }) => {
18
18
  requestOrigin = serverOrigin
19
19
  }
20
20
 
21
- const urlObject = new URL(nodeRequest.url, requestOrigin)
22
-
23
21
  return Object.freeze({
24
22
  signal,
25
23
  http2: Boolean(nodeRequest.stream),
26
- url: String(urlObject),
27
24
  origin: requestOrigin,
28
- pathname: urlObject.pathname,
29
- ressource: nodeRequest.url,
25
+ ...getPropertiesFromRessource({
26
+ ressource: nodeRequest.url,
27
+ baseUrl: requestOrigin,
28
+ }),
30
29
  method: nodeRequest.method,
31
30
  headers,
32
31
  body,
33
32
  })
34
33
  }
35
34
 
36
- export const createPushRequest = (request, { signal, pathname, method }) => {
37
- const childHeaders = { ...request.headers }
38
- // mtime sent by the client in request headers concerns the main request
39
- // Time remains valid for request to other ressources so we keep it
40
- // in child requests
41
- // delete childHeaders["if-modified-since"]
35
+ export const applyRedirectionToRequest = (
36
+ request,
37
+ { ressource, pathname, ...rest },
38
+ ) => {
39
+ return {
40
+ ...request,
41
+ ...(ressource
42
+ ? getPropertiesFromRessource({
43
+ ressource,
44
+ baseUrl: request.url,
45
+ })
46
+ : pathname
47
+ ? getPropertiesFromPathname({
48
+ pathname,
49
+ baseUrl: request.url,
50
+ })
51
+ : {}),
52
+ ...rest,
53
+ }
54
+ }
42
55
 
43
- // eTag sent by the client in request headers concerns the main request
44
- // A request made to an other ressource must not inherit the eTag
45
- delete childHeaders["if-none-match"]
56
+ const getPropertiesFromRessource = ({ ressource, baseUrl }) => {
57
+ const urlObject = new URL(ressource, baseUrl)
58
+ let pathname = urlObject.pathname
59
+
60
+ return {
61
+ url: String(urlObject),
62
+ pathname,
63
+ ressource,
64
+ }
65
+ }
46
66
 
67
+ const getPropertiesFromPathname = ({ pathname, baseUrl }) => {
68
+ return getPropertiesFromRessource({
69
+ ressource: `${pathname}${new URL(baseUrl).search}`,
70
+ baseUrl,
71
+ })
72
+ }
73
+
74
+ export const createPushRequest = (request, { signal, pathname, method }) => {
47
75
  const pushRequest = Object.freeze({
48
76
  ...request,
49
77
  parent: request,
50
78
  signal,
51
79
  http2: true,
52
- ...(pathname ? getRequestRedirectionProperties(request, { pathname }) : {}),
80
+ ...(pathname
81
+ ? getPropertiesFromPathname({
82
+ pathname,
83
+ baseUrl: request.url,
84
+ })
85
+ : {}),
53
86
  method: method || request.method,
54
- headers: childHeaders,
87
+ headers: getHeadersInheritedByPushRequest(request),
55
88
  body: undefined,
56
89
  })
57
-
58
90
  return pushRequest
59
91
  }
60
92
 
61
- const getRequestRedirectionProperties = (request, { ressource, pathname }) => {
62
- if (ressource === undefined) {
63
- const search = new URL(request.url).search
64
- ressource = `${pathname}${search}`
65
- }
66
- const url = `${request.origin}${ressource}`
67
- const urlObject = new URL(url)
68
- return {
69
- url,
70
- pathname: urlObject.pathname,
71
- ressource,
72
- }
73
- }
93
+ const getHeadersInheritedByPushRequest = (request) => {
94
+ const headersInherited = { ...request.headers }
95
+ // mtime sent by the client in request headers concerns the main request
96
+ // Time remains valid for request to other ressources so we keep it
97
+ // in child requests
98
+ // delete childHeaders["if-modified-since"]
74
99
 
75
- export const applyRedirectionToRequest = (
76
- request,
77
- { ressource, pathname, ...rest },
78
- ) => {
79
- return {
80
- ...request,
81
- ...getRequestRedirectionProperties(request, { ressource, pathname }),
82
- ...rest,
83
- }
100
+ // eTag sent by the client in request headers concerns the main request
101
+ // A request made to an other ressource must not inherit the eTag
102
+ delete headersInherited["if-none-match"]
103
+
104
+ return headersInherited
84
105
  }
@@ -16,8 +16,6 @@ export const pluginRequestWaitingCheck = ({
16
16
  } = {}) => {
17
17
  return {
18
18
  plugin_request_waiting_check: {
19
- init: () => {},
20
-
21
19
  onRequest: (request, { logger }) => {
22
20
  const timeout = setTimeout(
23
21
  () => requestWaitingCallback({ request, logger, requestWaitingMs }),
@@ -0,0 +1,55 @@
1
+ import { urlToMeta, normalizeStructuredMetaMap } from "@jsenv/url-meta"
2
+
3
+ export const pluginRessourceAliases = (ressourceAliases) => {
4
+ const ressourceAliasMap = {}
5
+ Object.keys(ressourceAliases).forEach((key) => {
6
+ ressourceAliasMap[ressourceAliases[key]] = key
7
+ })
8
+ const structuredMetaMap = normalizeStructuredMetaMap(
9
+ { alias: ressourceAliasMap },
10
+ "file:///",
11
+ )
12
+
13
+ const getRessourceAlias = (pathname) => {
14
+ const { alias } = urlToMeta({
15
+ url: `file://${pathname}`,
16
+ structuredMetaMap,
17
+ })
18
+ return alias
19
+ }
20
+
21
+ return {
22
+ ressource_aliases: {
23
+ onRequest: (request, { redirectRequest }) => {
24
+ const ressourceAlias = getRessourceAlias(request.pathname)
25
+ if (!ressourceAlias) {
26
+ return
27
+ }
28
+
29
+ const urlObjectAfterAlias = replaceUrlRessource(
30
+ request.url,
31
+ ressourceAlias,
32
+ )
33
+ const ressource = `${urlObjectAfterAlias.pathname}${urlObjectAfterAlias.search}`
34
+ redirectRequest({
35
+ ressourceBeforeAlias: request.ressource,
36
+ ressource,
37
+ })
38
+ },
39
+ },
40
+ }
41
+ }
42
+
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
50
+ }
51
+
52
+ const pathname = ressource.slice(0, searchSeparatorIndex)
53
+ urlObject.pathname = pathname
54
+ return urlObject
55
+ }
@@ -0,0 +1,35 @@
1
+ import { createRequire } from "node:module"
2
+
3
+ const require = createRequire(import.meta.url)
4
+
5
+ export const setupRoutes = (routes) => {
6
+ const { match } = require("path-to-regexp")
7
+
8
+ const candidates = Object.keys(routes).map((pathPattern) => {
9
+ const applyPatternMatching = match(pathPattern, {
10
+ decode: decodeURIComponent,
11
+ })
12
+ return {
13
+ applyPatternMatching,
14
+ requestHandler: routes[pathPattern],
15
+ }
16
+ })
17
+
18
+ return (request, { pushResponse, redirectRequest }) => {
19
+ let result
20
+ const found = candidates.find((candidate) => {
21
+ result = candidate.applyPatternMatching(request.pathname)
22
+ return Boolean(result)
23
+ })
24
+ if (found) {
25
+ return found.requestHandler(
26
+ {
27
+ ...request,
28
+ routeParams: result.params,
29
+ },
30
+ { pushResponse, redirectRequest },
31
+ )
32
+ }
33
+ return null
34
+ }
35
+ }
@@ -1,37 +1,13 @@
1
1
  import { timeFunction } from "@jsenv/server/src/server_timing/timing_measure.js"
2
2
  import { composeTwoResponses } from "@jsenv/server/src/internal/response_composition.js"
3
- import { applyRedirectionToRequest } from "@jsenv/server/src/internal/nodeRequestToRequest.js"
3
+ import { applyRedirectionToRequest } from "@jsenv/server/src/internal/request_factory.js"
4
4
 
5
- const requestRedirectionSymbol = Symbol("request_redirection")
6
-
7
- export const redirectRequest = (redirection) => {
8
- return {
9
- [requestRedirectionSymbol]: redirection,
10
- }
11
- }
12
-
13
- export const composeServices = (...callbacks) => {
14
- return (request, ...rest) => {
15
- return firstOperationMatching({
16
- array: callbacks,
17
- start: (callback) => callback(request, ...rest),
18
- predicate: (returnValue) => {
19
- if (returnValue === null || typeof returnValue !== "object") {
20
- return false
21
- }
22
- const requestRedirection = returnValue[requestRedirectionSymbol]
23
- if (requestRedirection) {
24
- request = applyRedirectionToRequest(request, requestRedirection)
25
- return false
26
- }
27
- return true
28
- },
29
- })
30
- }
31
- }
5
+ export const composeServices = (namedServices) => {
6
+ return async (request, { pushResponse }) => {
7
+ const redirectRequest = (requestRedirection) => {
8
+ request = applyRedirectionToRequest(request, requestRedirection)
9
+ }
32
10
 
33
- export const composeServicesWithTiming = (namedServices) => {
34
- return async (request, ...rest) => {
35
11
  const servicesTiming = {}
36
12
  const response = await firstOperationMatching({
37
13
  array: Object.keys(namedServices).map((serviceName) => {
@@ -39,7 +15,7 @@ export const composeServicesWithTiming = (namedServices) => {
39
15
  }),
40
16
  start: async ({ serviceName, serviceFn }) => {
41
17
  const [serviceTiming, value] = await timeFunction(serviceName, () =>
42
- serviceFn(request, ...rest),
18
+ serviceFn(request, { pushResponse, redirectRequest }),
43
19
  )
44
20
  Object.assign(servicesTiming, serviceTiming)
45
21
  return value
@@ -48,11 +24,6 @@ export const composeServicesWithTiming = (namedServices) => {
48
24
  if (returnValue === null || typeof returnValue !== "object") {
49
25
  return false
50
26
  }
51
- const requestRedirection = returnValue[requestRedirectionSymbol]
52
- if (requestRedirection) {
53
- request = applyRedirectionToRequest(request, requestRedirection)
54
- return false
55
- }
56
27
  return true
57
28
  },
58
29
  })
@@ -12,9 +12,10 @@ import { createPolyglotServer } from "./internal/server-polyglot.js"
12
12
  import { trackServerPendingConnections } from "./internal/trackServerPendingConnections.js"
13
13
  import { trackServerPendingRequests } from "./internal/trackServerPendingRequests.js"
14
14
  import {
15
- nodeRequestToRequest,
15
+ fromNodeRequest,
16
16
  createPushRequest,
17
- } from "./internal/nodeRequestToRequest.js"
17
+ applyRedirectionToRequest,
18
+ } from "./internal/request_factory.js"
18
19
  import { populateNodeResponse } from "./internal/populateNodeResponse.js"
19
20
  import { colorizeResponseStatus } from "./internal/colorizeResponseStatus.js"
20
21
  import { getServerOrigins } from "./internal/getServerOrigins.js"
@@ -265,9 +266,9 @@ export const startServer = async ({
265
266
  return stopCallbackList.add(abort)
266
267
  })
267
268
 
268
- const request = nodeRequestToRequest(nodeRequest, {
269
- signal: receiveRequestOperation.signal,
269
+ const request = fromNodeRequest(nodeRequest, {
270
270
  serverOrigin,
271
+ signal: receiveRequestOperation.signal,
271
272
  })
272
273
 
273
274
  // Handling request is asynchronous, we buffer logs for that request
@@ -417,6 +418,7 @@ export const startServer = async ({
417
418
  pathname: path,
418
419
  method,
419
420
  })
421
+
420
422
  try {
421
423
  const responseProperties = await handleRequest(pushRequest, {
422
424
  requestNode,
@@ -473,12 +475,17 @@ export const startServer = async ({
473
475
  const shortcircuitResponse = (value) => {
474
476
  responseFromShortcircuit = value
475
477
  }
478
+ const redirectRequest = (requestRedirection) => {
479
+ request = applyRedirectionToRequest(request, requestRedirection)
480
+ }
476
481
  Object.keys(plugins).forEach((pluginName) => {
477
482
  const { onRequest } = plugins[pluginName]
478
483
  if (onRequest) {
479
484
  const returnValue = onRequest(request, {
480
485
  warn,
486
+ logger,
481
487
  shortcircuitResponse,
488
+ redirectRequest,
482
489
  })
483
490
  if (returnValue) {
484
491
  requestPlugins.push(returnValue)
@@ -725,7 +732,9 @@ const generateResponseDescription = async ({
725
732
  pushResponse,
726
733
  }) => {
727
734
  try {
728
- const returnValue = await requestToResponse(request, { pushResponse })
735
+ const returnValue = await requestToResponse(request, {
736
+ pushResponse,
737
+ })
729
738
  return {
730
739
  responseProperties: returnValue,
731
740
  }