@jsenv/server 13.1.0 → 14.1.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/README.md CHANGED
@@ -43,7 +43,7 @@ const server = await startServer({
43
43
  {
44
44
  name: "index",
45
45
  handleRequest: (request) => {
46
- if (request.ressource === "/") {
46
+ if (request.resource === "/") {
47
47
  return { status: 200 }
48
48
  }
49
49
  return null
@@ -104,7 +104,7 @@ await startServer({
104
104
  services: [
105
105
  {
106
106
  handleRequest: async (request) => {
107
- const fileUrl = new URL(request.ressource.slice(1), import.meta.url)
107
+ const fileUrl = new URL(request.resource.slice(1), import.meta.url)
108
108
  const response = await fetchFileSystem(fileUrl, request)
109
109
  return response
110
110
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/server",
3
- "version": "13.1.0",
3
+ "version": "14.1.1",
4
4
  "description": "Write your Node.js server using pure functions",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "@jsenv/abort": "4.2.3",
38
- "@jsenv/log": "3.1.0",
38
+ "@jsenv/log": "3.2.0",
39
39
  "@jsenv/url-meta": "7.0.0",
40
40
  "@jsenv/utils": "2.0.1",
41
41
  "ws": "8.8.1"
@@ -64,7 +64,7 @@ export const fetchFileSystem = async (
64
64
  }
65
65
 
66
66
  // here you might be tempted to add || cacheControl === 'no-cache'
67
- // but no-cache means ressource can be cache but must be revalidated (yeah naming is strange)
67
+ // but no-cache means resource can be cache but must be revalidated (yeah naming is strange)
68
68
  // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#Cacheability
69
69
  if (cacheControl === "no-store") {
70
70
  if (etagEnabled) {
@@ -206,7 +206,7 @@ const getClientCacheResponse = async ({
206
206
  sourceUrl,
207
207
  }) => {
208
208
  // here you might be tempted to add || headers["cache-control"] === "no-cache"
209
- // but no-cache means ressource can be cache but must be revalidated (yeah naming is strange)
209
+ // but no-cache means resource can be cache but must be revalidated (yeah naming is strange)
210
210
  // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#Cacheability
211
211
 
212
212
  if (
@@ -0,0 +1,48 @@
1
+ import { isIP } from "node:net"
2
+
3
+ export const parseHostname = (hostname) => {
4
+ if (hostname === "0.0.0.0") {
5
+ return {
6
+ type: "ip",
7
+ label: "unspecified",
8
+ version: 4,
9
+ }
10
+ }
11
+ if (
12
+ hostname === "::" ||
13
+ hostname === "0000:0000:0000:0000:0000:0000:0000:0000"
14
+ ) {
15
+ return {
16
+ type: "ip",
17
+ label: "unspecified",
18
+ version: 6,
19
+ }
20
+ }
21
+ if (hostname === "127.0.0.1") {
22
+ return {
23
+ type: "ip",
24
+ label: "loopback",
25
+ version: 4,
26
+ }
27
+ }
28
+ if (
29
+ hostname === "::1" ||
30
+ hostname === "0000:0000:0000:0000:0000:0000:0000:0001"
31
+ ) {
32
+ return {
33
+ type: "ip",
34
+ label: "loopback",
35
+ version: 6,
36
+ }
37
+ }
38
+ const ipVersion = isIP(hostname)
39
+ if (ipVersion === 0) {
40
+ return {
41
+ type: "hostname",
42
+ }
43
+ }
44
+ return {
45
+ type: "ip",
46
+ version: ipVersion,
47
+ }
48
+ }
@@ -6,7 +6,7 @@ const listen = async ({
6
6
  server,
7
7
  port,
8
8
  portHint,
9
- host,
9
+ hostname,
10
10
  }) => {
11
11
  const listeningOperation = Abort.startOperation()
12
12
 
@@ -17,11 +17,11 @@ const listen = async ({
17
17
  listeningOperation.throwIfAborted()
18
18
  port = await findFreePort(portHint, {
19
19
  signal: listeningOperation.signal,
20
- host,
20
+ hostname,
21
21
  })
22
22
  }
23
23
  listeningOperation.throwIfAborted()
24
- port = await startListening({ server, port, host })
24
+ port = await startListening({ server, port, hostname })
25
25
  listeningOperation.addAbortCallback(() => stopListening(server))
26
26
  listeningOperation.throwIfAborted()
27
27
 
@@ -35,7 +35,7 @@ export const findFreePort = async (
35
35
  initialPort = 1,
36
36
  {
37
37
  signal = new AbortController().signal,
38
- host = "127.0.0.1",
38
+ hostname = "127.0.0.1",
39
39
  min = 1,
40
40
  max = 65534,
41
41
  next = (port) => port + 1,
@@ -56,26 +56,26 @@ export const findFreePort = async (
56
56
  const nextPort = next(port)
57
57
  if (nextPort > max) {
58
58
  throw new Error(
59
- `${host} has no available port between ${min} and ${max}`,
59
+ `${hostname} has no available port between ${min} and ${max}`,
60
60
  )
61
61
  }
62
- return testUntil(nextPort, host)
62
+ return testUntil(nextPort, hostname)
63
63
  }
64
- const freePort = await testUntil(initialPort, host)
64
+ const freePort = await testUntil(initialPort, hostname)
65
65
  return freePort
66
66
  } finally {
67
67
  await findFreePortOperation.end()
68
68
  }
69
69
  }
70
70
 
71
- const portIsFree = async (port, host) => {
71
+ const portIsFree = async (port, hostname) => {
72
72
  const server = createServer()
73
73
 
74
74
  try {
75
75
  await startListening({
76
76
  server,
77
77
  port,
78
- host,
78
+ hostname,
79
79
  })
80
80
  } catch (error) {
81
81
  if (error && error.code === "EADDRINUSE") {
@@ -91,7 +91,7 @@ const portIsFree = async (port, host) => {
91
91
  return true
92
92
  }
93
93
 
94
- const startListening = ({ server, port, host }) => {
94
+ const startListening = ({ server, port, hostname }) => {
95
95
  return new Promise((resolve, reject) => {
96
96
  server.on("error", reject)
97
97
  server.on("listening", () => {
@@ -99,7 +99,7 @@ const startListening = ({ server, port, host }) => {
99
99
  // https://nodejs.org/api/net.html#net_server_listen_port_host_backlog_callback
100
100
  resolve(server.address().port)
101
101
  })
102
- server.listen(port, host)
102
+ server.listen(port, hostname)
103
103
  })
104
104
  }
105
105
 
@@ -38,7 +38,7 @@ export const nodeStreamToObservable = (nodeStream) => {
38
38
  // used in the next ${readableStreamLifetimeInSeconds} otherwise destroys it
39
39
  const timeout = setTimeout(() => {
40
40
  process.emitWarning(
41
- `Readable stream not used after ${readableStreamLifetimeInSeconds} seconds. It will be destroyed to release ressources`,
41
+ `Readable stream not used after ${readableStreamLifetimeInSeconds} seconds. It will be destroyed to release resources`,
42
42
  {
43
43
  CODE: "READABLE_STREAM_TIMEOUT",
44
44
  // url is for http client request
@@ -24,8 +24,8 @@ export const fromNodeRequest = (nodeRequest, { serverOrigin, signal }) => {
24
24
  signal,
25
25
  http2: Boolean(nodeRequest.stream),
26
26
  origin: requestOrigin,
27
- ...getPropertiesFromRessource({
28
- ressource: nodeRequest.url,
27
+ ...getPropertiesFromResource({
28
+ resource: nodeRequest.url,
29
29
  baseUrl: requestOrigin,
30
30
  }),
31
31
  method: nodeRequest.method,
@@ -36,13 +36,13 @@ export const fromNodeRequest = (nodeRequest, { serverOrigin, signal }) => {
36
36
 
37
37
  export const applyRedirectionToRequest = (
38
38
  request,
39
- { ressource, pathname, ...rest },
39
+ { resource, pathname, ...rest },
40
40
  ) => {
41
41
  return {
42
42
  ...request,
43
- ...(ressource
44
- ? getPropertiesFromRessource({
45
- ressource,
43
+ ...(resource
44
+ ? getPropertiesFromResource({
45
+ resource,
46
46
  baseUrl: request.url,
47
47
  })
48
48
  : pathname
@@ -55,20 +55,20 @@ export const applyRedirectionToRequest = (
55
55
  }
56
56
  }
57
57
 
58
- const getPropertiesFromRessource = ({ ressource, baseUrl }) => {
59
- const urlObject = new URL(ressource, baseUrl)
58
+ const getPropertiesFromResource = ({ resource, baseUrl }) => {
59
+ const urlObject = new URL(resource, baseUrl)
60
60
  let pathname = urlObject.pathname
61
61
 
62
62
  return {
63
63
  url: String(urlObject),
64
64
  pathname,
65
- ressource,
65
+ resource,
66
66
  }
67
67
  }
68
68
 
69
69
  const getPropertiesFromPathname = ({ pathname, baseUrl }) => {
70
- return getPropertiesFromRessource({
71
- ressource: `${pathname}${new URL(baseUrl).search}`,
70
+ return getPropertiesFromResource({
71
+ resource: `${pathname}${new URL(baseUrl).search}`,
72
72
  baseUrl,
73
73
  })
74
74
  }
@@ -95,12 +95,12 @@ export const createPushRequest = (request, { signal, pathname, method }) => {
95
95
  const getHeadersInheritedByPushRequest = (request) => {
96
96
  const headersInherited = { ...request.headers }
97
97
  // mtime sent by the client in request headers concerns the main request
98
- // Time remains valid for request to other ressources so we keep it
98
+ // Time remains valid for request to other resources so we keep it
99
99
  // in child requests
100
100
  // delete childHeaders["if-modified-since"]
101
101
 
102
102
  // eTag sent by the client in request headers concerns the main request
103
- // A request made to an other ressource must not inherit the eTag
103
+ // A request made to an other resource must not inherit the eTag
104
104
  delete headersInherited["if-none-match"]
105
105
 
106
106
  return headersInherited
@@ -0,0 +1,49 @@
1
+ import { networkInterfaces } from "node:os"
2
+
3
+ export const createIpGetters = () => {
4
+ const networkAddresses = []
5
+ const networkInterfaceMap = networkInterfaces()
6
+ for (const key of Object.keys(networkInterfaceMap)) {
7
+ for (const networkAddress of networkInterfaceMap[key]) {
8
+ networkAddresses.push(networkAddress)
9
+ }
10
+ }
11
+ return {
12
+ getFirstInternalIp: ({ preferIpv6 }) => {
13
+ const isPref = preferIpv6 ? isIpV6 : isIpV4
14
+ let firstInternalIp
15
+ for (const networkAddress of networkAddresses) {
16
+ if (networkAddress.internal) {
17
+ firstInternalIp = networkAddress.address
18
+ if (isPref(networkAddress)) {
19
+ break
20
+ }
21
+ }
22
+ }
23
+ return firstInternalIp
24
+ },
25
+ getFirstExternalIp: ({ preferIpv6 }) => {
26
+ const isPref = preferIpv6 ? isIpV6 : isIpV4
27
+ let firstExternalIp
28
+ for (const networkAddress of networkAddresses) {
29
+ if (!networkAddress.internal) {
30
+ firstExternalIp = networkAddress.address
31
+ if (isPref(networkAddress)) {
32
+ break
33
+ }
34
+ }
35
+ }
36
+ return firstExternalIp
37
+ },
38
+ }
39
+ }
40
+
41
+ const isIpV4 = (networkAddress) => {
42
+ // node 18.5
43
+ if (typeof networkAddress.family === "number") {
44
+ return networkAddress.family === 4
45
+ }
46
+ return networkAddress.family === "IPv4"
47
+ }
48
+
49
+ const isIpV6 = (networkAddress) => !isIpV4(networkAddress)
package/src/main.js CHANGED
@@ -42,5 +42,5 @@ export { jsenvServiceResponseAcceptanceCheck } from "./services/response_accepta
42
42
  export { serveDirectory } from "./serve_directory.js"
43
43
  export { fromFetchResponse } from "./from_fetch_response.js"
44
44
  export { composeTwoResponses } from "./internal/response_composition.js"
45
- export { jsenvServiceRessourceAliases } from "./services/ressource_aliases/jsenv_service_ressource_aliases.js"
45
+ export { jsenvServiceRequestAliases } from "./services/request_aliases/jsenv_service_request_aliases.js"
46
46
  export { findFreePort } from "./internal/listen.js"
@@ -0,0 +1,37 @@
1
+ import { URL_META } from "@jsenv/url-meta"
2
+
3
+ export const jsenvServiceRequestAliases = (resourceAliases) => {
4
+ const aliases = {}
5
+ Object.keys(resourceAliases).forEach((key) => {
6
+ aliases[asFileUrl(key)] = asFileUrl(resourceAliases[key])
7
+ })
8
+ return {
9
+ name: "jsenv:request_aliases",
10
+ redirectRequest: (request) => {
11
+ const resourceBeforeAlias = request.resource
12
+ const urlAfterAliasing = URL_META.applyAliases({
13
+ url: asFileUrl(request.pathname),
14
+ aliases,
15
+ })
16
+ const resourceAfterAlias = urlAfterAliasing.slice("file://".length)
17
+ if (resourceBeforeAlias === resourceAfterAlias) {
18
+ return null
19
+ }
20
+ const resource = replaceResource(resourceBeforeAlias, resourceAfterAlias)
21
+ return { resource }
22
+ },
23
+ }
24
+ }
25
+
26
+ const asFileUrl = (specifier) => new URL(specifier, "file:///").href
27
+
28
+ const replaceResource = (resourceBeforeAlias, newValue) => {
29
+ const urlObject = new URL(resourceBeforeAlias, "file://")
30
+ const searchSeparatorIndex = newValue.indexOf("?")
31
+ if (searchSeparatorIndex > -1) {
32
+ return newValue // let new value override search params
33
+ }
34
+ urlObject.pathname = newValue
35
+ const resource = `${urlObject.pathname}${urlObject.search}`
36
+ return resource
37
+ }
@@ -37,9 +37,7 @@ export const createSSERoom = ({
37
37
 
38
38
  const lastKnownId =
39
39
  request.headers["last-event-id"] ||
40
- new URL(request.ressource, request.origin).searchParams.get(
41
- "last-event-id",
42
- )
40
+ new URL(request.url).searchParams.get("last-event-id")
43
41
 
44
42
  if (clients.size >= maxClientAllowed) {
45
43
  return {
@@ -1,3 +1,4 @@
1
+ import { isIP } from "node:net"
1
2
  import http from "node:http"
2
3
  import cluster from "node:cluster"
3
4
  import { createDetailedMessage, createLogger } from "@jsenv/log"
@@ -23,7 +24,6 @@ import {
23
24
  statusToType,
24
25
  colorizeResponseStatus,
25
26
  } from "./internal/colorizeResponseStatus.js"
26
- import { getServerOrigins } from "./internal/getServerOrigins.js"
27
27
  import { listen, stopListening } from "./internal/listen.js"
28
28
  import { composeTwoResponses } from "./internal/response_composition.js"
29
29
  import { listenRequest } from "./internal/listenRequest.js"
@@ -40,6 +40,10 @@ import {
40
40
  } from "./stopReasons.js"
41
41
  import { composeTwoHeaders } from "./internal/headers_composition.js"
42
42
 
43
+ import { createIpGetters } from "./internal/server_ips.js"
44
+ import { parseHostname } from "./internal/hostname_parser.js"
45
+ import { applyDnsResolution } from "./internal/dns_resolution.js"
46
+
43
47
  export const startServer = async ({
44
48
  signal = new AbortController().signal,
45
49
  logLevel,
@@ -52,7 +56,8 @@ export const startServer = async ({
52
56
  redirectHttpToHttps,
53
57
  allowHttpRequestOnHttps = false,
54
58
  acceptAnyIp = false,
55
- host = acceptAnyIp ? undefined : "localhost",
59
+ preferIpv6,
60
+ hostname = "localhost",
56
61
  port = 0, // assign a random available port
57
62
  portHint,
58
63
  privateKey,
@@ -76,13 +81,15 @@ export const startServer = async ({
76
81
  createDetailedMessage(
77
82
  `still no response found for request after ${requestWaitingMs} ms`,
78
83
  {
79
- "request url": `${request.origin}${request.ressource}`,
84
+ "request url": request.url,
80
85
  "request headers": JSON.stringify(request.headers, null, " "),
81
86
  },
82
87
  ),
83
88
  )
84
89
  },
85
90
  } = {}) => {
91
+ const logger = createLogger({ logLevel })
92
+
86
93
  if (protocol !== "http" && protocol !== "https") {
87
94
  throw new Error(`protocol must be http or https, got ${protocol}`)
88
95
  }
@@ -98,7 +105,6 @@ export const startServer = async ({
98
105
  throw new Error(`http2 needs "https" but protocol is "${protocol}"`)
99
106
  }
100
107
 
101
- const logger = createLogger({ logLevel })
102
108
  if (
103
109
  redirectHttpToHttps === undefined &&
104
110
  protocol === "https" &&
@@ -137,6 +143,9 @@ export const startServer = async ({
137
143
  let nodeServer
138
144
  const startServerOperation = Abort.startOperation()
139
145
  const stopCallbackList = createCallbackListNotifiedOnce()
146
+ const serverOrigins = {
147
+ local: "", // favors hostname when possible
148
+ }
140
149
 
141
150
  try {
142
151
  startServerOperation.addAbortSignal(signal)
@@ -163,12 +172,73 @@ export const startServer = async ({
163
172
  nodeServer.unref()
164
173
  }
165
174
 
175
+ const createOrigin = (hostname) => {
176
+ if (isIP(hostname) === 6) {
177
+ return `${protocol}://[${hostname}]`
178
+ }
179
+ return `${protocol}://${hostname}`
180
+ }
181
+
182
+ const ipGetters = createIpGetters()
183
+ let hostnameToListen
184
+ if (acceptAnyIp) {
185
+ const firstInternalIp = ipGetters.getFirstInternalIp({ preferIpv6 })
186
+ serverOrigins.local = createOrigin(firstInternalIp)
187
+ serverOrigins.localip = createOrigin(firstInternalIp)
188
+ const firstExternalIp = ipGetters.getFirstExternalIp({ preferIpv6 })
189
+ serverOrigins.externalip = createOrigin(firstExternalIp)
190
+ hostnameToListen = preferIpv6 ? "::" : "0.0.0.0"
191
+ } else {
192
+ hostnameToListen = hostname
193
+ }
194
+ const hostnameInfo = parseHostname(hostname)
195
+ if (hostnameInfo.type === "ip") {
196
+ if (acceptAnyIp) {
197
+ throw new Error(
198
+ `hostname cannot be an ip when acceptAnyIp is enabled, got ${hostname}`,
199
+ )
200
+ }
201
+
202
+ preferIpv6 = hostnameInfo.version === 6
203
+ const firstInternalIp = ipGetters.getFirstInternalIp({ preferIpv6 })
204
+ serverOrigins.local = createOrigin(firstInternalIp)
205
+ serverOrigins.localip = createOrigin(firstInternalIp)
206
+ if (hostnameInfo.label === "unspecified") {
207
+ const firstExternalIp = ipGetters.getFirstExternalIp({ preferIpv6 })
208
+ serverOrigins.externalip = createOrigin(firstExternalIp)
209
+ } else if (hostnameInfo.label === "loopback") {
210
+ } else {
211
+ serverOrigins.local = createOrigin(hostname)
212
+ }
213
+ } else {
214
+ const hostnameDnsResolution = await applyDnsResolution(hostname, {
215
+ verbatim: true,
216
+ })
217
+ if (hostnameDnsResolution) {
218
+ const hostnameIp = hostnameDnsResolution.address
219
+ serverOrigins.localip = createOrigin(hostnameIp)
220
+ serverOrigins.local = createOrigin(hostname)
221
+ } else {
222
+ const firstInternalIp = ipGetters.getFirstInternalIp({ preferIpv6 })
223
+ // fallback to internal ip because there is no ip
224
+ // associated to this hostname on operating system (in hosts file)
225
+ hostname = firstInternalIp
226
+ hostnameToListen = firstInternalIp
227
+ serverOrigins.local = createOrigin(firstInternalIp)
228
+ }
229
+ }
230
+
166
231
  port = await listen({
167
232
  signal: startServerOperation.signal,
168
233
  server: nodeServer,
169
234
  port,
170
235
  portHint,
171
- host,
236
+ hostname: hostnameToListen,
237
+ })
238
+
239
+ // normalize origins (remove :80 when port is 80 for instance)
240
+ Object.keys(serverOrigins).forEach((key) => {
241
+ serverOrigins[key] = new URL(`${serverOrigins[key]}:${port}`).origin
172
242
  })
173
243
 
174
244
  serviceController.callHooks("serverListening", { port })
@@ -180,6 +250,18 @@ export const startServer = async ({
180
250
  await startServerOperation.end()
181
251
  }
182
252
 
253
+ // the main server origin
254
+ // - when protocol is http
255
+ // node-fetch do not apply local dns resolution to map localhost back to 127.0.0.1
256
+ // despites localhost being mapped so we prefer to use the internal ip
257
+ // (127.0.0.1)
258
+ // - when protocol is https
259
+ // using the hostname becomes important because the certificate is generated
260
+ // for hostnames, not for ips
261
+ // so we prefer https://locahost or https://local_hostname
262
+ // over the ip
263
+ const serverOrigin = serverOrigins.local
264
+
183
265
  // now the server is started (listening) it cannot be aborted anymore
184
266
  // (otherwise an AbortError is thrown to the code calling "startServer")
185
267
  // we can proceed to create a stop function to stop it gacefully
@@ -218,8 +300,6 @@ export const startServer = async ({
218
300
  }
219
301
 
220
302
  status = "opened"
221
- const serverOrigins = await getServerOrigins({ protocol, host, port })
222
- const serverOrigin = serverOrigins.local
223
303
 
224
304
  const removeConnectionErrorListener = listenServerConnectionError(
225
305
  nodeServer,
@@ -387,7 +467,7 @@ export const startServer = async ({
387
467
  addRequestLog(requestNode, {
388
468
  type: "error",
389
469
  value: createDetailedMessage(
390
- `An error occured while pushing a stream to the response for ${request.ressource}`,
470
+ `An error occured while pushing a stream to the response for ${request.resource}`,
391
471
  {
392
472
  "error stack": e.stack,
393
473
  },
@@ -492,8 +572,8 @@ export const startServer = async ({
492
572
  addRequestLog(requestNode, {
493
573
  type: "info",
494
574
  value: request.parent
495
- ? `Push ${request.ressource}`
496
- : `${request.method} ${request.origin}${request.ressource}`,
575
+ ? `Push ${request.resource}`
576
+ : `${request.method} ${request.url}`,
497
577
  })
498
578
  const warn = (value) => {
499
579
  addRequestLog(requestNode, {
@@ -829,7 +909,9 @@ export const startServer = async ({
829
909
  const { WebSocketServer } = await import("ws")
830
910
  let websocketServer = new WebSocketServer({ noServer: true })
831
911
  const websocketOrigin =
832
- protocol === "https" ? `wss://${host}:${port}` : `ws://${host}:${port}`
912
+ protocol === "https"
913
+ ? `wss://${hostname}:${port}`
914
+ : `ws://${hostname}:${port}`
833
915
  server.websocketOrigin = websocketOrigin
834
916
  const upgradeCallback = (nodeRequest, socket, head) => {
835
917
  websocketServer.handleUpgrade(
@@ -884,6 +966,7 @@ export const startServer = async ({
884
966
  Object.assign(server, {
885
967
  getStatus: () => status,
886
968
  port,
969
+ hostname,
887
970
  origin: serverOrigin,
888
971
  origins: serverOrigins,
889
972
  nodeServer,
@@ -1,74 +0,0 @@
1
- import { networkInterfaces } from "node:os"
2
-
3
- import { applyDnsResolution } from "./dns_resolution.js"
4
-
5
- export const getServerOrigins = async ({ protocol, host, port }) => {
6
- const isLocal = LOOPBACK_HOSTNAMES.includes(host)
7
- const localhostDnsResolution = await applyDnsResolution("localhost")
8
- const localOrigin = createServerOrigin({
9
- protocol,
10
- hostname:
11
- localhostDnsResolution.address === "127.0.0.1"
12
- ? "localhost"
13
- : "127.0.0.1",
14
- port,
15
- })
16
- if (isLocal) {
17
- return { local: localOrigin }
18
- }
19
- const isAnyIp = WILDCARD_HOSTNAMES.includes(host)
20
- const networkOrigin = createServerOrigin({
21
- protocol,
22
- hostname: isAnyIp ? getExternalIp() : host,
23
- port,
24
- })
25
- return {
26
- local: localOrigin,
27
- network: networkOrigin,
28
- }
29
- }
30
-
31
- const LOOPBACK_HOSTNAMES = [
32
- "localhost",
33
- "127.0.0.1",
34
- "::1",
35
- "0000:0000:0000:0000:0000:0000:0000:0001",
36
- ]
37
-
38
- const WILDCARD_HOSTNAMES = [
39
- undefined,
40
- "0.0.0.0",
41
- "::",
42
- "0000:0000:0000:0000:0000:0000:0000:0000",
43
- ]
44
-
45
- const createServerOrigin = ({ protocol, hostname, port }) => {
46
- const url = new URL("https://127.0.0.1:80")
47
- url.protocol = protocol
48
- url.hostname = hostname
49
- url.port = port
50
- return url.origin
51
- }
52
-
53
- const getExternalIp = () => {
54
- const networkInterfaceMap = networkInterfaces()
55
- let internalIPV4NetworkAddress
56
- Object.keys(networkInterfaceMap).find((key) => {
57
- const networkAddressArray = networkInterfaceMap[key]
58
- return networkAddressArray.find((networkAddress) => {
59
- if (networkAddress.internal) return false
60
- if (!isIpV4(networkAddress)) return false
61
- internalIPV4NetworkAddress = networkAddress
62
- return true
63
- })
64
- })
65
- return internalIPV4NetworkAddress ? internalIPV4NetworkAddress.address : null
66
- }
67
-
68
- const isIpV4 = (networkAddress) => {
69
- // node 18+
70
- if (typeof networkAddress.family === "number") {
71
- return networkAddress.family === 4
72
- }
73
- return networkAddress.family === "IPv4"
74
- }
@@ -1,40 +0,0 @@
1
- import { URL_META } from "@jsenv/url-meta"
2
-
3
- export const jsenvServiceRessourceAliases = (ressourceAliases) => {
4
- const aliases = {}
5
- Object.keys(ressourceAliases).forEach((key) => {
6
- aliases[asFileUrl(key)] = asFileUrl(ressourceAliases[key])
7
- })
8
- return {
9
- name: "jsenv:ressource_aliases",
10
- redirectRequest: (request) => {
11
- const ressourceBeforeAlias = request.ressource
12
- const urlAfterAliasing = URL_META.applyAliases({
13
- url: asFileUrl(request.pathname),
14
- aliases,
15
- })
16
- const ressourceAfterAlias = urlAfterAliasing.slice("file://".length)
17
- if (ressourceBeforeAlias === ressourceAfterAlias) {
18
- return null
19
- }
20
- const ressource = replaceRessource(
21
- ressourceBeforeAlias,
22
- ressourceAfterAlias,
23
- )
24
- return { ressource }
25
- },
26
- }
27
- }
28
-
29
- const asFileUrl = (specifier) => new URL(specifier, "file:///").href
30
-
31
- const replaceRessource = (ressourceBeforeAlias, newValue) => {
32
- const urlObject = new URL(ressourceBeforeAlias, "file://")
33
- const searchSeparatorIndex = newValue.indexOf("?")
34
- if (searchSeparatorIndex > -1) {
35
- return newValue // let new value override search params
36
- }
37
- urlObject.pathname = newValue
38
- const ressource = `${urlObject.pathname}${urlObject.search}`
39
- return ressource
40
- }