@serwist/routing
Advanced tools
Comparing version 9.0.0-preview.0 to 9.0.0-preview.1
import { assert, logger, getFriendlyURL, SerwistError } from '@serwist/core/internal'; | ||
/* | ||
Copyright 2018 Google LLC | ||
Use of this source code is governed by an MIT-style | ||
license that can be found in the LICENSE file or at | ||
https://opensource.org/licenses/MIT. | ||
*/ /** | ||
* The default HTTP method, 'GET', used when there's no specific method | ||
* configured for a route. | ||
* | ||
* @private | ||
*/ const defaultMethod = "GET"; | ||
/** | ||
* The list of valid HTTP methods associated with requests that could be routed. | ||
* | ||
* @private | ||
*/ const validMethods = [ | ||
const defaultMethod = "GET"; | ||
const validMethods = [ | ||
"DELETE", | ||
@@ -28,9 +13,3 @@ "GET", | ||
/** | ||
* @param handler Either a function, or an object with a | ||
* 'handle' method. | ||
* @returns An object with a handle method. | ||
* | ||
* @private | ||
*/ const normalizeHandler = (handler)=>{ | ||
const normalizeHandler = (handler)=>{ | ||
if (handler && typeof handler === "object") { | ||
@@ -60,9 +39,3 @@ if (process.env.NODE_ENV !== "production") { | ||
/** | ||
* A `Route` consists of a pair of callback functions, "match" and "handler". | ||
* The "match" callback determine if a route should be used to "handle" a | ||
* request by returning a non-falsy value if it can. The "handler" callback | ||
* is called when there is a match and should return a Promise that resolves | ||
* to a `Response`. | ||
*/ class Route { | ||
class Route { | ||
handler; | ||
@@ -72,12 +45,3 @@ match; | ||
catchHandler; | ||
/** | ||
* Constructor for Route class. | ||
* | ||
* @param match A callback function that determines whether the | ||
* route matches a given `fetch` event by returning a non-falsy value. | ||
* @param handler A callback function that returns a Promise resolving | ||
* to a Response. | ||
* @param method The HTTP method to match the Route against. Defaults | ||
* to GET. | ||
*/ constructor(match, handler, method = defaultMethod){ | ||
constructor(match, handler, method = defaultMethod){ | ||
if (process.env.NODE_ENV !== "production") { | ||
@@ -96,4 +60,2 @@ assert.isType(match, "function", { | ||
} | ||
// These values are referenced directly by Router so cannot be | ||
// altered by minificaton. | ||
this.handler = normalizeHandler(handler); | ||
@@ -103,7 +65,3 @@ this.match = match; | ||
} | ||
/** | ||
* | ||
* @param handler A callback function that returns a Promise resolving | ||
* to a Response. | ||
*/ setCatchHandler(handler) { | ||
setCatchHandler(handler) { | ||
this.catchHandler = normalizeHandler(handler); | ||
@@ -113,31 +71,6 @@ } | ||
/** | ||
* NavigationRoute makes it easy to create a `@serwist/routing` Route that matches for browser | ||
* [navigation requests](https://developers.google.com/web/fundamentals/primers/service-workers/high-performance-loading#first_what_are_navigation_requests). | ||
* | ||
* It will only match incoming Requests whose [mode](https://fetch.spec.whatwg.org/#concept-request-mode) is set to `navigate`. | ||
* | ||
* You can optionally only apply this route to a subset of navigation requests | ||
* by using one or both of the `denylist` and `allowlist` parameters. | ||
*/ class NavigationRoute extends Route { | ||
class NavigationRoute extends Route { | ||
_allowlist; | ||
_denylist; | ||
/** | ||
* If both `denylist` and `allowlist` are provided, the `denylist` will | ||
* take precedence and the request will not match this route. | ||
* | ||
* The regular expressions in `allowlist` and `denylist` | ||
* are matched against the concatenated | ||
* [`pathname`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLHyperlinkElementUtils/pathname) | ||
* and [`search`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLHyperlinkElementUtils/search) | ||
* portions of the requested URL. | ||
* | ||
* *Note*: These RegExps may be evaluated against every destination URL during | ||
* a navigation. Avoid using | ||
* [complex RegExps](https://github.com/GoogleChrome/workbox/issues/3077), | ||
* or else your users may see delays when navigating your site. | ||
* | ||
* @param handler A callback function that returns a Promise resulting in a Response. | ||
* @param options | ||
*/ constructor(handler, { allowlist = [ | ||
constructor(handler, { allowlist = [ | ||
/./ | ||
@@ -163,9 +96,3 @@ ], denylist = [] } = {}){ | ||
} | ||
/** | ||
* Routes match handler. | ||
* | ||
* @param options | ||
* @returns | ||
* @private | ||
*/ _match({ url, request }) { | ||
_match({ url, request }) { | ||
if (request && request.mode !== "navigate") { | ||
@@ -196,19 +123,4 @@ return false; | ||
/** | ||
* RegExpRoute makes it easy to create a regular expression based on a `@serwist/routing` Route. | ||
* | ||
* For same-origin requests the RegExp only needs to match part of the URL. For | ||
* requests against third-party servers, you must define a RegExp that matches | ||
* the start of the URL. | ||
*/ class RegExpRoute extends Route { | ||
/** | ||
* If the regular expression contains | ||
* [capture groups](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#grouping-back-references), | ||
* the captured values will be passed to the `params` argument. | ||
* | ||
* @param regExp The regular expression to match against URLs. | ||
* @param handler A callback function that returns a Promise resulting in a Response. | ||
* @param method The HTTP method to match the Route, defaults to GET. | ||
* against. | ||
*/ constructor(regExp, handler, method){ | ||
class RegExpRoute extends Route { | ||
constructor(regExp, handler, method){ | ||
if (process.env.NODE_ENV !== "production") { | ||
@@ -224,10 +136,5 @@ assert.isInstance(regExp, RegExp, { | ||
const result = regExp.exec(url.href); | ||
// Return immediately if there's no match. | ||
if (!result) { | ||
return; | ||
} | ||
// Require that the match start at the first character in the URL string | ||
// if it's a cross-origin request. | ||
// See https://github.com/GoogleChrome/workbox/issues/281 for the context | ||
// behind this behavior. | ||
if (url.origin !== location.origin && result.index !== 0) { | ||
@@ -239,6 +146,2 @@ if (process.env.NODE_ENV !== "production") { | ||
} | ||
// If the route matches, but there aren't any capture groups defined, then | ||
// this will return [], which is truthy and therefore sufficient to | ||
// indicate a match. | ||
// If there are capture groups, then it will return their values. | ||
return result.slice(1); | ||
@@ -250,34 +153,14 @@ }; | ||
/** | ||
* The Router can be used to process a `FetchEvent` using one or more `@serwist/routing` Route(s), | ||
* responding with a `Response` if a matching route exists. | ||
* | ||
* If no route matches a given a request, the Router will use a "default" handler if one is defined. | ||
* | ||
* Should the matching Route throw an error, the Router will use a "catch" handler if one is defined to | ||
* gracefully deal with issues and respond with a Request. | ||
* | ||
* If a request matches multiple routes, the **earliest** registered route will | ||
* be used to respond to the request. | ||
*/ class Router { | ||
class Router { | ||
_routes; | ||
_defaultHandlerMap; | ||
_catchHandler; | ||
/** | ||
* Initializes a new Router. | ||
*/ constructor(){ | ||
constructor(){ | ||
this._routes = new Map(); | ||
this._defaultHandlerMap = new Map(); | ||
} | ||
/** | ||
* @returns routes A `Map` of HTTP method name ('GET', etc.) to an array of all the corresponding `Route` | ||
* instances that are registered. | ||
*/ get routes() { | ||
get routes() { | ||
return this._routes; | ||
} | ||
/** | ||
* Adds a fetch event listener to respond to events when a route matches | ||
* the event's request. | ||
*/ addFetchListener() { | ||
// See https://github.com/Microsoft/TypeScript/issues/28357#issuecomment-436484705 | ||
addFetchListener() { | ||
self.addEventListener("fetch", (event)=>{ | ||
@@ -294,30 +177,5 @@ const { request } = event; | ||
} | ||
/** | ||
* Adds a message event listener for URLs to cache from the window. | ||
* This is useful to cache resources loaded on the page prior to when the | ||
* service worker started controlling it. | ||
* | ||
* The format of the message data sent from the window should be as follows. | ||
* Where the `urlsToCache` array may consist of URL strings or an array of | ||
* URL string + `requestInit` object (the same as you'd pass to `fetch()`). | ||
* | ||
* ``` | ||
* { | ||
* type: 'CACHE_URLS', | ||
* payload: { | ||
* urlsToCache: [ | ||
* './script1.js', | ||
* './script2.js', | ||
* ['./script3.js', {mode: 'no-cors'}], | ||
* ], | ||
* }, | ||
* } | ||
* ``` | ||
*/ addCacheListener() { | ||
// See https://github.com/Microsoft/TypeScript/issues/28357#issuecomment-436484705 | ||
addCacheListener() { | ||
self.addEventListener("message", (event)=>{ | ||
// event.data is type 'any' | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access | ||
if (event.data && event.data.type === "CACHE_URLS") { | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment | ||
const { payload } = event.data; | ||
@@ -340,3 +198,2 @@ if (process.env.NODE_ENV !== "production") { | ||
event.waitUntil(requestPromises); | ||
// If a MessageChannel was used, reply to the message on success. | ||
if (event.ports?.[0]) { | ||
@@ -348,11 +205,3 @@ void requestPromises.then(()=>event.ports[0].postMessage(true)); | ||
} | ||
/** | ||
* Apply the routing rules to a FetchEvent object to get a Response from an | ||
* appropriate Route's handler. | ||
* | ||
* @param options | ||
* @returns A promise is returned if a registered route can handle the request. | ||
* If there is no matching route and there's no `defaultHandler`, `undefined` | ||
* is returned. | ||
*/ handleRequest({ request, event }) { | ||
handleRequest({ request, event }) { | ||
if (process.env.NODE_ENV !== "production") { | ||
@@ -396,4 +245,2 @@ assert.isInstance(request, Request, { | ||
} | ||
// If we don't have a handler because there was no matching route, then | ||
// fall back to defaultHandler if that's defined. | ||
const method = request.method; | ||
@@ -408,4 +255,2 @@ if (!handler && this._defaultHandlerMap.has(method)) { | ||
if (process.env.NODE_ENV !== "production") { | ||
// No handler so Serwist will do nothing. If logs is set of debug | ||
// i.e. verbose, we should print out this information. | ||
logger.debug(`No route found for: ${getFriendlyURL(url)}`); | ||
@@ -416,4 +261,2 @@ } | ||
if (process.env.NODE_ENV !== "production") { | ||
// We have a handler, meaning Serwist is going to handle the route. | ||
// print the routing details to the console. | ||
logger.groupCollapsed(`Router is responding to: ${getFriendlyURL(url)}`); | ||
@@ -429,4 +272,2 @@ for (const msg of debugMessages){ | ||
} | ||
// Wrap in try and catch in case the handle method throws a synchronous | ||
// error. It should still callback to the catch handler. | ||
let responsePromise; | ||
@@ -443,11 +284,7 @@ try { | ||
} | ||
// Get route's catch handler, if it exists | ||
const catchHandler = route?.catchHandler; | ||
if (responsePromise instanceof Promise && (this._catchHandler || catchHandler)) { | ||
responsePromise = responsePromise.catch(async (err)=>{ | ||
// If there's a route catch handler, process that first | ||
if (catchHandler) { | ||
if (process.env.NODE_ENV !== "production") { | ||
// Still include URL here as it will be async from the console group | ||
// and may not make sense without the URL | ||
logger.groupCollapsed(`Error thrown when responding to: ${getFriendlyURL(url)}. Falling back to route's Catch Handler.`); | ||
@@ -473,4 +310,2 @@ logger.error("Error thrown by:", route); | ||
if (process.env.NODE_ENV !== "production") { | ||
// Still include URL here as it will be async from the console group | ||
// and may not make sense without the URL | ||
logger.groupCollapsed(`Error thrown when responding to: ${getFriendlyURL(url)}. Falling back to global Catch Handler.`); | ||
@@ -492,16 +327,6 @@ logger.error("Error thrown by:", route); | ||
} | ||
/** | ||
* Checks a request and URL (and optionally an event) against the list of | ||
* registered routes, and if there's a match, returns the corresponding | ||
* route along with any params generated by the match. | ||
* | ||
* @param options | ||
* @returns An object with `route` and `params` properties. They are populated | ||
* if a matching route was found or `undefined` otherwise. | ||
*/ findMatchingRoute({ url, sameOrigin, request, event }) { | ||
findMatchingRoute({ url, sameOrigin, request, event }) { | ||
const routes = this._routes.get(request.method) || []; | ||
for (const route of routes){ | ||
let params; | ||
// route.match returns type any, not possible to change right now. | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment | ||
const matchResult = route.match({ | ||
@@ -515,4 +340,2 @@ url, | ||
if (process.env.NODE_ENV !== "production") { | ||
// Warn developers that using an async matchCallback is almost always | ||
// not the right thing to do. | ||
if (matchResult instanceof Promise) { | ||
@@ -522,19 +345,10 @@ logger.warn(`While routing ${getFriendlyURL(url)}, an async matchCallback function was used. Please convert the following route to use a synchronous matchCallback function:`, route); | ||
} | ||
// See https://github.com/GoogleChrome/workbox/issues/2079 | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment | ||
params = matchResult; | ||
if (Array.isArray(params) && params.length === 0) { | ||
// Instead of passing an empty array in as params, use undefined. | ||
params = undefined; | ||
} else if (matchResult.constructor === Object && // eslint-disable-line | ||
Object.keys(matchResult).length === 0) { | ||
// Instead of passing an empty object in as params, use undefined. | ||
} else if (matchResult.constructor === Object && Object.keys(matchResult).length === 0) { | ||
params = undefined; | ||
} else if (typeof matchResult === "boolean") { | ||
// For the boolean value true (rather than just something truth-y), | ||
// don't set params. | ||
// See https://github.com/GoogleChrome/workbox/pull/2134#issuecomment-513924353 | ||
params = undefined; | ||
} | ||
// Return early if have a match. | ||
return { | ||
@@ -546,34 +360,11 @@ route, | ||
} | ||
// If no match was found above, return and empty object. | ||
return {}; | ||
} | ||
/** | ||
* Define a default `handler` that's called when no routes explicitly | ||
* match the incoming request. | ||
* | ||
* Each HTTP method ('GET', 'POST', etc.) gets its own default handler. | ||
* | ||
* Without a default handler, unmatched requests will go against the | ||
* network as if there were no service worker present. | ||
* | ||
* @param handler A callback function that returns a Promise resulting in a Response. | ||
* @param method The HTTP method to associate with this default handler. Each method | ||
* has its own default. Defaults to GET. | ||
*/ setDefaultHandler(handler, method = defaultMethod) { | ||
setDefaultHandler(handler, method = defaultMethod) { | ||
this._defaultHandlerMap.set(method, normalizeHandler(handler)); | ||
} | ||
/** | ||
* If a Route throws an error while handling a request, this `handler` | ||
* will be called and given a chance to provide a response. | ||
* | ||
* @param handler A callback function that returns a Promise resulting | ||
* in a Response. | ||
*/ setCatchHandler(handler) { | ||
setCatchHandler(handler) { | ||
this._catchHandler = normalizeHandler(handler); | ||
} | ||
/** | ||
* Registers a route with the router. | ||
* | ||
* @param route The route to register. | ||
*/ registerRoute(route) { | ||
registerRoute(route) { | ||
if (process.env.NODE_ENV !== "production") { | ||
@@ -614,11 +405,5 @@ assert.isType(route, "object", { | ||
} | ||
// Give precedence to all of the earlier routes by adding this additional | ||
// route to the end of the array. | ||
this._routes.get(route.method).push(route); | ||
} | ||
/** | ||
* Unregisters a route with the router. | ||
* | ||
* @param route The route to unregister. | ||
*/ unregisterRoute(route) { | ||
unregisterRoute(route) { | ||
if (!this._routes.has(route.method)) { | ||
@@ -639,12 +424,5 @@ throw new SerwistError("unregister-route-but-not-found-with-method", { | ||
let defaultRouter; | ||
/** | ||
* Creates a new, singleton Router instance if one does not exist. If one | ||
* does already exist, that instance is returned. | ||
* | ||
* @private | ||
* @returns | ||
*/ const getOrCreateDefaultRouter = ()=>{ | ||
const getOrCreateDefaultRouter = ()=>{ | ||
if (!defaultRouter) { | ||
defaultRouter = new Router(); | ||
// The helpers that use the default Router assume these listeners exist. | ||
defaultRouter.addFetchListener(); | ||
@@ -656,12 +434,3 @@ defaultRouter.addCacheListener(); | ||
/** | ||
* Registers a RegExp, string, or function with a caching | ||
* strategy to a singleton Router instance. | ||
* | ||
* @param capture If the capture param is a `Route`, all other arguments will be ignored. | ||
* @param handler A callback function that returns a Promise resulting in a Response. | ||
* This parameter is required if `capture` is not a `Route` object. | ||
* @param method The HTTP method to match the Route against. Defaults to GET. | ||
* @returns The generated `Route`. | ||
*/ const registerRoute = (capture, handler, method)=>{ | ||
const registerRoute = (capture, handler, method)=>{ | ||
let route; | ||
@@ -678,6 +447,3 @@ if (typeof capture === "string") { | ||
} | ||
// 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 = "[*:?+]"; | ||
@@ -696,9 +462,6 @@ if (new RegExp(`${wildcards}`).exec(valueToCheck)) { | ||
}; | ||
// 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); | ||
@@ -719,8 +482,3 @@ } else if (capture instanceof Route) { | ||
/** | ||
* If a Route throws an error while handling a request, this `handler` | ||
* will be called and given a chance to provide a response. | ||
* | ||
* @param handler A callback function that returns a Promise resulting in a Response. | ||
*/ const setCatchHandler = (handler)=>{ | ||
const setCatchHandler = (handler)=>{ | ||
const defaultRouter = getOrCreateDefaultRouter(); | ||
@@ -730,11 +488,3 @@ defaultRouter.setCatchHandler(handler); | ||
/** | ||
* Defines a default `handler` that's called when no routes explicitly | ||
* match the incoming request. | ||
* | ||
* Without a default handler, unmatched requests will go against the | ||
* network as if there were no service worker present. | ||
* | ||
* @param handler A callback function that returns a Promise resulting in a Response. | ||
*/ const setDefaultHandler = (handler)=>{ | ||
const setDefaultHandler = (handler)=>{ | ||
const defaultRouter = getOrCreateDefaultRouter(); | ||
@@ -744,7 +494,3 @@ defaultRouter.setDefaultHandler(handler); | ||
/** | ||
* Unregisters a route from the singleton Router instance. | ||
* | ||
* @param route The route to unregister. | ||
*/ const unregisterRoute = (route)=>{ | ||
const unregisterRoute = (route)=>{ | ||
const defaultRouter = getOrCreateDefaultRouter(); | ||
@@ -751,0 +497,0 @@ defaultRouter.unregisterRoute(route); |
{ | ||
"name": "@serwist/routing", | ||
"version": "9.0.0-preview.0", | ||
"version": "9.0.0-preview.1", | ||
"type": "module", | ||
@@ -33,3 +33,3 @@ "description": "A service worker helper library to route request URLs to handlers.", | ||
"dependencies": { | ||
"@serwist/core": "9.0.0-preview.0" | ||
"@serwist/core": "9.0.0-preview.1" | ||
}, | ||
@@ -39,3 +39,3 @@ "devDependencies": { | ||
"typescript": "5.4.0-dev.20240203", | ||
"@serwist/constants": "9.0.0-preview.0" | ||
"@serwist/constants": "9.0.0-preview.1" | ||
}, | ||
@@ -42,0 +42,0 @@ "peerDependencies": { |
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
75579
1724
+ Added@serwist/core@9.0.0-preview.1(transitive)
- Removed@serwist/core@9.0.0-preview.0(transitive)