@jsenv/server
Advanced tools
Comparing version 12.8.0 to 13.0.0
{ | ||
"name": "@jsenv/server", | ||
"version": "12.8.0", | ||
"version": "13.0.0", | ||
"description": "Write your Node.js server using pure functions", | ||
@@ -5,0 +5,0 @@ "license": "MIT", |
116
README.md
@@ -11,11 +11,15 @@ # server [](https://www.npmjs.com/package/@jsenv/server) | ||
port: 8080, | ||
requestToResponse: () => { | ||
return { | ||
status: 200, | ||
headers: { | ||
"content-type": "text/plain", | ||
services: [ | ||
{ | ||
handleRequest: () => { | ||
return { | ||
status: 200, | ||
headers: { | ||
"content-type": "text/plain", | ||
}, | ||
body: "Hello world", | ||
} | ||
}, | ||
body: "Hello world", | ||
} | ||
}, | ||
}, | ||
], | ||
}) | ||
@@ -33,11 +37,15 @@ ``` | ||
const server = await startServer({ | ||
requestToResponse: () => { | ||
return { | ||
status: 200, | ||
headers: { | ||
"content-type": "text/plain", | ||
services: [ | ||
{ | ||
handleRequest: () => { | ||
return { | ||
status: 200, | ||
headers: { | ||
"content-type": "text/plain", | ||
}, | ||
body: "Hello world", | ||
} | ||
}, | ||
body: "Hello world", | ||
} | ||
}, | ||
}, | ||
], | ||
}) | ||
@@ -62,18 +70,20 @@ | ||
const indexService = (request) => { | ||
if (request.ressource === "/") { | ||
return { status: 200 } | ||
} | ||
return null // means "I don't handle that request" | ||
} | ||
const notFoundService = (request) => { | ||
return { status: 404 } | ||
} | ||
const server = await startServer({ | ||
requestToResponse: composeServices({ | ||
index: indexService, | ||
otherwise: notFoundService, | ||
}), | ||
services: [ | ||
{ | ||
name: "index", | ||
handleRequest: (request) => { | ||
if (request.ressource === "/") { | ||
return { status: 200 } | ||
} | ||
return null // means "I don't handle that request" | ||
}, | ||
}, | ||
{ | ||
name: "otherwise", | ||
handleRequest: () => { | ||
return { status: 404 } | ||
}, | ||
}, | ||
], | ||
}) | ||
@@ -100,13 +110,17 @@ | ||
allowHttpRequestOnHttps: true, | ||
requestToResponse: (request) => { | ||
const clientUsesHttp = request.origin.startsWith("http:") | ||
services: [ | ||
{ | ||
handleRequest: (request) => { | ||
const clientUsesHttp = request.origin.startsWith("http:") | ||
return { | ||
status: 200, | ||
headers: { | ||
"content-type": "text/plain", | ||
return { | ||
status: 200, | ||
headers: { | ||
"content-type": "text/plain", | ||
}, | ||
body: clientUsesHttp ? `Welcome http user` : `Welcome https user`, | ||
} | ||
}, | ||
body: clientUsesHttp ? `Welcome http user` : `Welcome https user`, | ||
} | ||
}, | ||
}, | ||
], | ||
}) | ||
@@ -126,9 +140,13 @@ | ||
await startServer({ | ||
requestToResponse: async (request) => { | ||
const fileUrl = new URL(request.ressource.slice(1), import.meta.url) | ||
const response = await fetchFileSystem(fileUrl, { | ||
...request, | ||
}) | ||
return response | ||
}, | ||
services: [ | ||
{ | ||
handleRequest: async (request) => { | ||
const fileUrl = new URL(request.ressource.slice(1), import.meta.url) | ||
const response = await fetchFileSystem(fileUrl, { | ||
...request, | ||
}) | ||
return response | ||
}, | ||
}, | ||
], | ||
}) | ||
@@ -139,4 +157,2 @@ ``` | ||
- [https](./docs/https/https.md) | ||
- [Serving files](./docs/serving_files/serving_files.md) | ||
- [Handling requests](./docs/handling_requests/handling_requests.md) | ||
@@ -146,2 +162,4 @@ - [Handling errors](./docs/handling_errors/handling_errors.md) | ||
- [CORS](./docs/cors/cors.md) | ||
- [https](./docs/https/https.md) | ||
- [Serving files](./docs/serving_files/serving_files.md) | ||
- [Content negotiation](./docs/content_negotiation/content_negotiation.md) | ||
@@ -148,0 +166,0 @@ - [Server Sent Events](./docs/sse/sse.md) |
@@ -15,3 +15,3 @@ /* | ||
import { timeFunction } from "./server_timing/timing_measure.js" | ||
import { negotiateContentEncoding } from "./content_negotiation/negotiateContentEncoding.js" | ||
import { pickContentEncoding } from "./content_negotiation/pick_content_encoding.js" | ||
import { serveDirectory } from "./serve_directory.js" | ||
@@ -364,3 +364,3 @@ | ||
const getCompressedResponse = async ({ sourceUrl, headers }) => { | ||
const acceptedCompressionFormat = negotiateContentEncoding( | ||
const acceptedCompressionFormat = pickContentEncoding( | ||
{ headers }, | ||
@@ -367,0 +367,0 @@ Object.keys(availableCompressionFormats), |
import { networkInterfaces } from "node:os" | ||
import { lookup } from "node:dns" | ||
export const getServerOrigins = async ({ protocol, ip, port }) => { | ||
const isInternalIp = ip === "127.0.0.1" | ||
import { applyDnsResolution } from "./dns_resolution.js" | ||
export const getServerOrigins = async ({ protocol, host, port }) => { | ||
const isLocal = LOOPBACK_HOSTNAMES.includes(host) | ||
const localhostDnsResolution = await applyDnsResolution("localhost") | ||
const internalOrigin = createServerOrigin({ | ||
const localOrigin = createServerOrigin({ | ||
protocol, | ||
@@ -15,29 +16,31 @@ hostname: | ||
}) | ||
if (isInternalIp) { | ||
return { internal: internalOrigin } | ||
if (isLocal) { | ||
return { local: localOrigin } | ||
} | ||
const isAnyIp = !ip || ip === "::" || ip === "0.0.0.0" | ||
const isAnyIp = WILDCARD_HOSTNAMES.includes(host) | ||
const networkOrigin = createServerOrigin({ | ||
protocol, | ||
hostname: isAnyIp ? getExternalIp() : host, | ||
port, | ||
}) | ||
return { | ||
internal: internalOrigin, | ||
external: createServerOrigin({ | ||
protocol, | ||
hostname: isAnyIp ? getExternalIp(ip) : ip, | ||
port, | ||
}), | ||
local: localOrigin, | ||
network: networkOrigin, | ||
} | ||
} | ||
const applyDnsResolution = async (hostname) => { | ||
const dnsResolution = await new Promise((resolve, reject) => { | ||
lookup(hostname, (error, address, family) => { | ||
if (error) { | ||
reject(error) | ||
} else { | ||
resolve({ address, family }) | ||
} | ||
}) | ||
}) | ||
return dnsResolution | ||
} | ||
const LOOPBACK_HOSTNAMES = [ | ||
"localhost", | ||
"127.0.0.1", | ||
"::1", | ||
"0000:0000:0000:0000:0000:0000:0000:0001", | ||
] | ||
const WILDCARD_HOSTNAMES = [ | ||
undefined, | ||
"0.0.0.0", | ||
"::", | ||
"0000:0000:0000:0000:0000:0000:0000:0000", | ||
] | ||
const createServerOrigin = ({ protocol, hostname, port }) => { | ||
@@ -58,3 +61,3 @@ const url = new URL("https://127.0.0.1:80") | ||
if (networkAddress.internal) return false | ||
if (networkAddress.family !== "IPv4") return false | ||
if (!isIpV4(networkAddress)) return false | ||
internalIPV4NetworkAddress = networkAddress | ||
@@ -66,1 +69,9 @@ return true | ||
} | ||
const isIpV4 = (networkAddress) => { | ||
// node 18+ | ||
if (typeof networkAddress.family === "number") { | ||
return networkAddress.family === 4 | ||
} | ||
return networkAddress.family === "IPv4" | ||
} |
@@ -9,3 +9,3 @@ import { createServer } from "node:net" | ||
portHint, | ||
ip, | ||
host, | ||
}) => { | ||
@@ -21,7 +21,7 @@ const listeningOperation = Abort.startOperation() | ||
signal: listeningOperation.signal, | ||
ip, | ||
host, | ||
}) | ||
} | ||
listeningOperation.throwIfAborted() | ||
port = await startListening({ server, port, ip }) | ||
port = await startListening({ server, port, host }) | ||
listeningOperation.addAbortCallback(() => stopListening(server)) | ||
@@ -40,3 +40,3 @@ listeningOperation.throwIfAborted() | ||
signal = new AbortController().signal, | ||
ip = "127.0.0.1", | ||
host = "127.0.0.1", | ||
min = 1, | ||
@@ -52,5 +52,5 @@ max = 65534, | ||
const testUntil = async (port, ip) => { | ||
const testUntil = async (port, host) => { | ||
findFreePortOperation.throwIfAborted() | ||
const free = await portIsFree(port, ip) | ||
const free = await portIsFree(port, host) | ||
if (free) { | ||
@@ -62,7 +62,9 @@ return port | ||
if (nextPort > max) { | ||
throw new Error(`${ip} has no available port between ${min} and ${max}`) | ||
throw new Error( | ||
`${host} has no available port between ${min} and ${max}`, | ||
) | ||
} | ||
return testUntil(nextPort, ip) | ||
return testUntil(nextPort, host) | ||
} | ||
const freePort = await testUntil(initialPort, ip) | ||
const freePort = await testUntil(initialPort, host) | ||
return freePort | ||
@@ -74,3 +76,3 @@ } finally { | ||
const portIsFree = async (port, ip) => { | ||
const portIsFree = async (port, host) => { | ||
const server = createServer() | ||
@@ -82,3 +84,3 @@ | ||
port, | ||
ip, | ||
host, | ||
}) | ||
@@ -99,3 +101,3 @@ } catch (error) { | ||
const startListening = ({ server, port, ip }) => { | ||
const startListening = ({ server, port, host }) => { | ||
return new Promise((resolve, reject) => { | ||
@@ -108,3 +110,3 @@ server.on("error", reject) | ||
}) | ||
server.listen(port, ip) | ||
server.listen(port, host) | ||
}) | ||
@@ -111,0 +113,0 @@ } |
@@ -7,4 +7,3 @@ /* | ||
export { startServer } from "./startServer.js" | ||
export { composeServices } from "./service_composition/service_composition.js" | ||
export { startServer } from "./server.js" | ||
export { setupRoutes } from "./service_composition/routing.js" | ||
@@ -22,22 +21,22 @@ export { readRequestBody } from "./readRequestBody.js" | ||
} from "./stopReasons.js" | ||
export { jsenvServiceErrorHandler } from "./services/error_handler/jsenv_service_error_handler.js" | ||
// CORS | ||
export { | ||
pluginCORS, | ||
jsenvServiceCORS, | ||
jsenvAccessControlAllowedHeaders, | ||
jsenvAccessControlAllowedMethods, | ||
} from "./cors/plugin_cors.js" | ||
} from "./services/cors/jsenv_service_cors.js" | ||
// server timings | ||
export { pluginServerTiming } from "./server_timing/plugin_server_timing.js" | ||
// server timing | ||
export { timeFunction, timeStart } from "./server_timing/timing_measure.js" | ||
// SSE | ||
export { createSSERoom } from "./sse/createSSERoom.js" | ||
export { createSSERoom } from "./sse/sse_room.js" | ||
// content-negotiation | ||
export { negotiateContentType } from "./content_negotiation/negotiateContentType.js" | ||
export { negotiateContentEncoding } from "./content_negotiation/negotiateContentEncoding.js" | ||
export { negotiateContentLanguage } from "./content_negotiation/negotiateContentLanguage.js" | ||
export { pluginContentNegotiationCheck } from "./content_negotiation/plugin_content_negotiation_check.js" | ||
export { pickContentType } from "./content_negotiation/pick_content_type.js" | ||
export { pickContentEncoding } from "./content_negotiation/pick_content_encoding.js" | ||
export { pickContentLanguage } from "./content_negotiation/pick_content_language.js" | ||
export { jsenvServiceResponseAcceptanceCheck } from "./services/response_acceptance_check/jsenv_service_response_acceptance_check.js" | ||
@@ -48,4 +47,3 @@ // others | ||
export { composeTwoResponses } from "./internal/response_composition.js" | ||
export { pluginRessourceAliases } from "./ressource_aliases/plugin_ressource_aliases.js" | ||
export { pluginRequestWaitingCheck } from "./plugin_request_waiting_check.js" | ||
export { jsenvServiceRessourceAliases } from "./services/ressource_aliases/jsenv_service_ressource_aliases.js" | ||
export { findFreePort } from "./internal/listen.js" |
import { readdirSync } from "node:fs" | ||
import { negotiateContentType } from "./content_negotiation/negotiateContentType.js" | ||
import { pickContentType } from "./content_negotiation/pick_content_type.js" | ||
@@ -60,3 +60,3 @@ export const serveDirectory = ( | ||
} | ||
const bestContentType = negotiateContentType( | ||
const bestContentType = pickContentType( | ||
{ headers }, | ||
@@ -63,0 +63,0 @@ Object.keys(responseProducers), |
143088
4547
168
52