@serwist/routing
Advanced tools
Comparing version 9.0.0-preview.16 to 9.0.0-preview.17
@@ -1,66 +0,5 @@ | ||
import { assert, logger, getFriendlyURL, SerwistError } from '@serwist/core/internal'; | ||
import { assert, logger } from '@serwist/core/internal'; | ||
import { R as Route, p as parseRoute, g as getOrCreateDefaultRouter } from './chunks/parseRoute.js'; | ||
export { a as RegExpRoute, b as Router } from './chunks/parseRoute.js'; | ||
const defaultMethod = "GET"; | ||
const validMethods = [ | ||
"DELETE", | ||
"GET", | ||
"HEAD", | ||
"PATCH", | ||
"POST", | ||
"PUT" | ||
]; | ||
const normalizeHandler = (handler)=>{ | ||
if (handler && typeof handler === "object") { | ||
if (process.env.NODE_ENV !== "production") { | ||
assert.hasMethod(handler, "handle", { | ||
moduleName: "@serwist/routing", | ||
className: "Route", | ||
funcName: "constructor", | ||
paramName: "handler" | ||
}); | ||
} | ||
return handler; | ||
} | ||
if (process.env.NODE_ENV !== "production") { | ||
assert.isType(handler, "function", { | ||
moduleName: "@serwist/routing", | ||
className: "Route", | ||
funcName: "constructor", | ||
paramName: "handler" | ||
}); | ||
} | ||
return { | ||
handle: handler | ||
}; | ||
}; | ||
class Route { | ||
handler; | ||
match; | ||
method; | ||
catchHandler; | ||
constructor(match, handler, method = defaultMethod){ | ||
if (process.env.NODE_ENV !== "production") { | ||
assert.isType(match, "function", { | ||
moduleName: "@serwist/routing", | ||
className: "Route", | ||
funcName: "constructor", | ||
paramName: "match" | ||
}); | ||
if (method) { | ||
assert.isOneOf(method, validMethods, { | ||
paramName: "method" | ||
}); | ||
} | ||
} | ||
this.handler = normalizeHandler(handler); | ||
this.match = match; | ||
this.method = method; | ||
} | ||
setCatchHandler(handler) { | ||
this.catchHandler = normalizeHandler(handler); | ||
} | ||
} | ||
class NavigationRoute extends Route { | ||
@@ -116,334 +55,4 @@ _allowlist; | ||
class RegExpRoute extends Route { | ||
constructor(regExp, handler, method){ | ||
if (process.env.NODE_ENV !== "production") { | ||
assert.isInstance(regExp, RegExp, { | ||
moduleName: "@serwist/routing", | ||
className: "RegExpRoute", | ||
funcName: "constructor", | ||
paramName: "pattern" | ||
}); | ||
} | ||
const match = ({ url })=>{ | ||
const result = regExp.exec(url.href); | ||
if (!result) { | ||
return; | ||
} | ||
if (url.origin !== location.origin && result.index !== 0) { | ||
if (process.env.NODE_ENV !== "production") { | ||
logger.debug(`The regular expression '${regExp.toString()}' only partially matched against the cross-origin URL '${url.toString()}'. RegExpRoute's will only handle cross-origin requests if they match the entire URL.`); | ||
} | ||
return; | ||
} | ||
return result.slice(1); | ||
}; | ||
super(match, handler, method); | ||
} | ||
} | ||
class Router { | ||
_routes; | ||
_defaultHandlerMap; | ||
_catchHandler; | ||
constructor(){ | ||
this._routes = new Map(); | ||
this._defaultHandlerMap = new Map(); | ||
} | ||
get routes() { | ||
return this._routes; | ||
} | ||
addFetchListener() { | ||
self.addEventListener("fetch", (event)=>{ | ||
const { request } = event; | ||
const responsePromise = this.handleRequest({ | ||
request, | ||
event | ||
}); | ||
if (responsePromise) { | ||
event.respondWith(responsePromise); | ||
} | ||
}); | ||
} | ||
addCacheListener() { | ||
self.addEventListener("message", (event)=>{ | ||
if (event.data && event.data.type === "CACHE_URLS") { | ||
const { payload } = event.data; | ||
if (process.env.NODE_ENV !== "production") { | ||
logger.debug("Caching URLs from the window", payload.urlsToCache); | ||
} | ||
const requestPromises = Promise.all(payload.urlsToCache.map((entry)=>{ | ||
if (typeof entry === "string") { | ||
entry = [ | ||
entry | ||
]; | ||
} | ||
const request = new Request(...entry); | ||
return this.handleRequest({ | ||
request, | ||
event | ||
}); | ||
})); | ||
event.waitUntil(requestPromises); | ||
if (event.ports?.[0]) { | ||
void requestPromises.then(()=>event.ports[0].postMessage(true)); | ||
} | ||
} | ||
}); | ||
} | ||
handleRequest({ request, event }) { | ||
if (process.env.NODE_ENV !== "production") { | ||
assert.isInstance(request, Request, { | ||
moduleName: "@serwist/routing", | ||
className: "Router", | ||
funcName: "handleRequest", | ||
paramName: "options.request" | ||
}); | ||
} | ||
const url = new URL(request.url, location.href); | ||
if (!url.protocol.startsWith("http")) { | ||
if (process.env.NODE_ENV !== "production") { | ||
logger.debug("The Serwist router only supports URLs that start with 'http'."); | ||
} | ||
return; | ||
} | ||
const sameOrigin = url.origin === location.origin; | ||
const { params, route } = this.findMatchingRoute({ | ||
event, | ||
request, | ||
sameOrigin, | ||
url | ||
}); | ||
let handler = route?.handler; | ||
const debugMessages = []; | ||
if (process.env.NODE_ENV !== "production") { | ||
if (handler) { | ||
debugMessages.push([ | ||
"Found a route to handle this request:", | ||
route | ||
]); | ||
if (params) { | ||
debugMessages.push([ | ||
`Passing the following params to the route's handler:`, | ||
params | ||
]); | ||
} | ||
} | ||
} | ||
const method = request.method; | ||
if (!handler && this._defaultHandlerMap.has(method)) { | ||
if (process.env.NODE_ENV !== "production") { | ||
debugMessages.push(`Failed to find a matching route. Falling back to the default handler for ${method}.`); | ||
} | ||
handler = this._defaultHandlerMap.get(method); | ||
} | ||
if (!handler) { | ||
if (process.env.NODE_ENV !== "production") { | ||
logger.debug(`No route found for: ${getFriendlyURL(url)}`); | ||
} | ||
return; | ||
} | ||
if (process.env.NODE_ENV !== "production") { | ||
logger.groupCollapsed(`Router is responding to: ${getFriendlyURL(url)}`); | ||
for (const msg of debugMessages){ | ||
if (Array.isArray(msg)) { | ||
logger.log(...msg); | ||
} else { | ||
logger.log(msg); | ||
} | ||
} | ||
logger.groupEnd(); | ||
} | ||
let responsePromise; | ||
try { | ||
responsePromise = handler.handle({ | ||
url, | ||
request, | ||
event, | ||
params | ||
}); | ||
} catch (err) { | ||
responsePromise = Promise.reject(err); | ||
} | ||
const catchHandler = route?.catchHandler; | ||
if (responsePromise instanceof Promise && (this._catchHandler || catchHandler)) { | ||
responsePromise = responsePromise.catch(async (err)=>{ | ||
if (catchHandler) { | ||
if (process.env.NODE_ENV !== "production") { | ||
logger.groupCollapsed(`Error thrown when responding to: ${getFriendlyURL(url)}. Falling back to route's Catch Handler.`); | ||
logger.error("Error thrown by:", route); | ||
logger.error(err); | ||
logger.groupEnd(); | ||
} | ||
try { | ||
return await catchHandler.handle({ | ||
url, | ||
request, | ||
event, | ||
params | ||
}); | ||
} catch (catchErr) { | ||
if (catchErr instanceof Error) { | ||
err = catchErr; | ||
} | ||
} | ||
} | ||
if (this._catchHandler) { | ||
if (process.env.NODE_ENV !== "production") { | ||
logger.groupCollapsed(`Error thrown when responding to: ${getFriendlyURL(url)}. Falling back to global Catch Handler.`); | ||
logger.error("Error thrown by:", route); | ||
logger.error(err); | ||
logger.groupEnd(); | ||
} | ||
return this._catchHandler.handle({ | ||
url, | ||
request, | ||
event | ||
}); | ||
} | ||
throw err; | ||
}); | ||
} | ||
return responsePromise; | ||
} | ||
findMatchingRoute({ url, sameOrigin, request, event }) { | ||
const routes = this._routes.get(request.method) || []; | ||
for (const route of routes){ | ||
let params; | ||
const matchResult = route.match({ | ||
url, | ||
sameOrigin, | ||
request, | ||
event | ||
}); | ||
if (matchResult) { | ||
if (process.env.NODE_ENV !== "production") { | ||
if (matchResult instanceof Promise) { | ||
logger.warn(`While routing ${getFriendlyURL(url)}, an async matchCallback function was used. Please convert the following route to use a synchronous matchCallback function:`, route); | ||
} | ||
} | ||
params = matchResult; | ||
if (Array.isArray(params) && params.length === 0) { | ||
params = undefined; | ||
} else if (matchResult.constructor === Object && Object.keys(matchResult).length === 0) { | ||
params = undefined; | ||
} else if (typeof matchResult === "boolean") { | ||
params = undefined; | ||
} | ||
return { | ||
route, | ||
params | ||
}; | ||
} | ||
} | ||
return {}; | ||
} | ||
setDefaultHandler(handler, method = defaultMethod) { | ||
this._defaultHandlerMap.set(method, normalizeHandler(handler)); | ||
} | ||
setCatchHandler(handler) { | ||
this._catchHandler = normalizeHandler(handler); | ||
} | ||
registerRoute(route) { | ||
if (process.env.NODE_ENV !== "production") { | ||
assert.isType(route, "object", { | ||
moduleName: "@serwist/routing", | ||
className: "Router", | ||
funcName: "registerRoute", | ||
paramName: "route" | ||
}); | ||
assert.hasMethod(route, "match", { | ||
moduleName: "@serwist/routing", | ||
className: "Router", | ||
funcName: "registerRoute", | ||
paramName: "route" | ||
}); | ||
assert.isType(route.handler, "object", { | ||
moduleName: "@serwist/routing", | ||
className: "Router", | ||
funcName: "registerRoute", | ||
paramName: "route" | ||
}); | ||
assert.hasMethod(route.handler, "handle", { | ||
moduleName: "@serwist/routing", | ||
className: "Router", | ||
funcName: "registerRoute", | ||
paramName: "route.handler" | ||
}); | ||
assert.isType(route.method, "string", { | ||
moduleName: "@serwist/routing", | ||
className: "Router", | ||
funcName: "registerRoute", | ||
paramName: "route.method" | ||
}); | ||
} | ||
if (!this._routes.has(route.method)) { | ||
this._routes.set(route.method, []); | ||
} | ||
this._routes.get(route.method).push(route); | ||
} | ||
unregisterRoute(route) { | ||
if (!this._routes.has(route.method)) { | ||
throw new SerwistError("unregister-route-but-not-found-with-method", { | ||
method: route.method | ||
}); | ||
} | ||
const routeIndex = this._routes.get(route.method).indexOf(route); | ||
if (routeIndex > -1) { | ||
this._routes.get(route.method).splice(routeIndex, 1); | ||
} else { | ||
throw new SerwistError("unregister-route-route-not-registered"); | ||
} | ||
} | ||
} | ||
let defaultRouter; | ||
const getOrCreateDefaultRouter = ()=>{ | ||
if (!defaultRouter) { | ||
defaultRouter = new Router(); | ||
defaultRouter.addFetchListener(); | ||
defaultRouter.addCacheListener(); | ||
} | ||
return defaultRouter; | ||
}; | ||
const registerRoute = (capture, handler, method)=>{ | ||
let route; | ||
if (typeof capture === "string") { | ||
const captureUrl = new URL(capture, location.href); | ||
if (process.env.NODE_ENV !== "production") { | ||
if (!(capture.startsWith("/") || capture.startsWith("http"))) { | ||
throw new SerwistError("invalid-string", { | ||
moduleName: "@serwist/routing", | ||
funcName: "registerRoute", | ||
paramName: "capture" | ||
}); | ||
} | ||
const valueToCheck = capture.startsWith("http") ? captureUrl.pathname : capture; | ||
const wildcards = "[*:?+]"; | ||
if (new RegExp(`${wildcards}`).exec(valueToCheck)) { | ||
logger.debug(`The '$capture' parameter contains an Express-style wildcard character (${wildcards}). Strings are now always interpreted as exact matches; use a RegExp for partial or wildcard matches.`); | ||
} | ||
} | ||
const matchCallback = ({ url })=>{ | ||
if (process.env.NODE_ENV !== "production") { | ||
if (url.pathname === captureUrl.pathname && url.origin !== captureUrl.origin) { | ||
logger.debug(`${capture} only partially matches the cross-origin URL ${url.toString()}. This route will only handle cross-origin requests if they match the entire URL.`); | ||
} | ||
} | ||
return url.href === captureUrl.href; | ||
}; | ||
route = new Route(matchCallback, handler, method); | ||
} else if (capture instanceof RegExp) { | ||
route = new RegExpRoute(capture, handler, method); | ||
} else if (typeof capture === "function") { | ||
route = new Route(capture, handler, method); | ||
} else if (capture instanceof Route) { | ||
route = capture; | ||
} else { | ||
throw new SerwistError("unsupported-route-type", { | ||
moduleName: "@serwist/routing", | ||
funcName: "registerRoute", | ||
paramName: "capture" | ||
}); | ||
} | ||
const route = parseRoute(capture, handler, method); | ||
const defaultRouter = getOrCreateDefaultRouter(); | ||
@@ -469,2 +78,2 @@ defaultRouter.registerRoute(route); | ||
export { NavigationRoute, RegExpRoute, Route, Router, registerRoute, setCatchHandler, setDefaultHandler, unregisterRoute }; | ||
export { NavigationRoute, Route, registerRoute, setCatchHandler, setDefaultHandler, unregisterRoute }; |
{ | ||
"name": "@serwist/routing", | ||
"version": "9.0.0-preview.16", | ||
"version": "9.0.0-preview.17", | ||
"type": "module", | ||
@@ -25,2 +25,9 @@ "description": "A service worker helper library to route request URLs to handlers.", | ||
"types": "./dist/index.d.ts", | ||
"typesVersions": { | ||
"*": { | ||
"internal": [ | ||
"./dist/index.internal.d.ts" | ||
] | ||
} | ||
}, | ||
"exports": { | ||
@@ -31,6 +38,10 @@ ".": { | ||
}, | ||
"./internal": { | ||
"types": "./dist/index.internal.d.ts", | ||
"default": "./dist/index.internal.js" | ||
}, | ||
"./package.json": "./package.json" | ||
}, | ||
"dependencies": { | ||
"@serwist/core": "9.0.0-preview.16" | ||
"@serwist/core": "9.0.0-preview.17" | ||
}, | ||
@@ -40,3 +51,3 @@ "devDependencies": { | ||
"typescript": "5.5.0-dev.20240323", | ||
"@serwist/constants": "9.0.0-preview.16" | ||
"@serwist/constants": "9.0.0-preview.17" | ||
}, | ||
@@ -43,0 +54,0 @@ "peerDependencies": { |
@@ -10,8 +10,7 @@ /* | ||
import type { RouteHandler, RouteMatchCallback } from "@serwist/core"; | ||
import { SerwistError, logger } from "@serwist/core/internal"; | ||
import { RegExpRoute } from "./RegExpRoute.js"; | ||
import { Route } from "./Route.js"; | ||
import type { HTTPMethod } from "./utils/constants.js"; | ||
import { getOrCreateDefaultRouter } from "./utils/getOrCreateDefaultRouter.js"; | ||
import { parseRoute } from "./utils/parseRoute.js"; | ||
@@ -29,59 +28,4 @@ /** | ||
export const registerRoute = (capture: RegExp | string | RouteMatchCallback | Route, handler?: RouteHandler, method?: HTTPMethod): Route => { | ||
let route: Route; | ||
const route = parseRoute(capture, handler, method); | ||
if (typeof capture === "string") { | ||
const captureUrl = new URL(capture, location.href); | ||
if (process.env.NODE_ENV !== "production") { | ||
if (!(capture.startsWith("/") || capture.startsWith("http"))) { | ||
throw new SerwistError("invalid-string", { | ||
moduleName: "@serwist/routing", | ||
funcName: "registerRoute", | ||
paramName: "capture", | ||
}); | ||
} | ||
// We want to check if Express-style wildcards are in the pathname only. | ||
// TODO: Remove this log message in v4. | ||
const valueToCheck = capture.startsWith("http") ? captureUrl.pathname : capture; | ||
// See https://github.com/pillarjs/path-to-regexp#parameters | ||
const wildcards = "[*:?+]"; | ||
if (new RegExp(`${wildcards}`).exec(valueToCheck)) { | ||
logger.debug( | ||
`The '$capture' parameter contains an Express-style wildcard character (${wildcards}). Strings are now always interpreted as exact matches; use a RegExp for partial or wildcard matches.`, | ||
); | ||
} | ||
} | ||
const matchCallback: RouteMatchCallback = ({ url }) => { | ||
if (process.env.NODE_ENV !== "production") { | ||
if (url.pathname === captureUrl.pathname && url.origin !== captureUrl.origin) { | ||
logger.debug( | ||
`${capture} only partially matches the cross-origin URL ${url.toString()}. This route will only handle cross-origin requests if they match the entire URL.`, | ||
); | ||
} | ||
} | ||
return url.href === captureUrl.href; | ||
}; | ||
// If `capture` is a string then `handler` and `method` must be present. | ||
route = new Route(matchCallback, handler!, method); | ||
} else if (capture instanceof RegExp) { | ||
// If `capture` is a `RegExp` then `handler` and `method` must be present. | ||
route = new RegExpRoute(capture, handler!, method); | ||
} else if (typeof capture === "function") { | ||
// If `capture` is a function then `handler` and `method` must be present. | ||
route = new Route(capture, handler!, method); | ||
} else if (capture instanceof Route) { | ||
route = capture; | ||
} else { | ||
throw new SerwistError("unsupported-route-type", { | ||
moduleName: "@serwist/routing", | ||
funcName: "registerRoute", | ||
paramName: "capture", | ||
}); | ||
} | ||
const defaultRouter = getOrCreateDefaultRouter(); | ||
@@ -88,0 +32,0 @@ defaultRouter.registerRoute(route); |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Unidentified License
License(Experimental) Something that seems like a license was found, but its contents could not be matched with a known license.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Unidentified License
License(Experimental) Something that seems like a license was found, but its contents could not be matched with a known license.
Found 1 instance in 1 package
77707
51
1747
+ Added@serwist/core@9.0.0-preview.17(transitive)
- Removed@serwist/core@9.0.0-preview.16(transitive)