extendRouter - greydragon888/real-router GitHub Wiki
getPluginApi().extendRouter
Method for plugin authors
1. Overview
- What it does: Assigns properties directly to the router instance, with conflict detection and automatic cleanup. Returns an unsubscribe function that removes all added properties.
- When to use:
- Adding convenience methods to the router from a plugin (e.g.,
buildUrl,matchUrl) - Extending the router's public API surface from a plugin
- Any case where a plugin needs to add properties to the router instance
- Adding convenience methods to the router from a plugin (e.g.,
2. Signature
import { getPluginApi } from "@real-router/core/api";
const pluginApi = getPluginApi(router);
pluginApi.extendRouter(
extensions: Record<string, unknown>,
): Unsubscribe
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
extensions |
Record<string, unknown> |
Yes | Object whose keys and values will be assigned to the router instance. |
Return Value
type Unsubscribe = () => void;
- Returns a function that removes all added properties from the router
- Idempotent โ safe to call multiple times
- Typically called during plugin
teardown()
3. Behavior
Conflict Detection
Before assigning any property, extendRouter validates that none of the keys already exist on the router instance. If any key conflicts, a RouterError(PLUGIN_CONFLICT) is thrown and no properties are assigned.
const api = getPluginApi(router);
// First plugin extends router โ OK
api.extendRouter({ buildUrl: () => "/url" });
// Second plugin tries the same key โ throws
api.extendRouter({ buildUrl: () => "/other" });
// RouterError: PLUGIN_CONFLICT โ "Cannot extend router: property "buildUrl" already exists"
Atomicity
Validation uses a two-loop pattern: all keys are checked first, then all are assigned. If any key conflicts, the entire call fails โ no partial assignment occurs.
// If "a" exists but "b" doesn't, neither is assigned
api.extendRouter({ a: 1, b: 2 }); // throws โ "a" conflicts, "b" is NOT assigned
Cleanup via Unsubscribe
The returned function removes all properties that were added by this specific extendRouter call:
const removeExtensions = api.extendRouter({
buildUrl: buildUrlImpl,
matchUrl: matchUrlImpl,
});
router.buildUrl; // โ
exists
removeExtensions();
router.buildUrl; // โ undefined โ property removed
The unsubscribe function is idempotent โ calling it multiple times is safe.
Safety Net on dispose()
Extensions are tracked in RouterInternals.routerExtensions. During router.dispose(), any remaining extensions are cleaned up automatically as a safety net. This handles cases where a plugin's teardown fails or forgets to call its unsubscribe function.
Disposal Guard
Calling extendRouter on a disposed router throws RouterError(ROUTER_DISPOSED):
router.dispose();
api.extendRouter({ myMethod: fn }); // throws RouterError(DISPOSED)
4. Possible Errors
| Condition | Error | Code |
|---|---|---|
| Key already exists | RouterError |
PLUGIN_CONFLICT |
| Router disposed | RouterError |
DISPOSED |
5. Type Safety with Module Augmentation
extendRouter assigns properties at runtime. To get TypeScript support for the added properties, use declare module augmentation:
// In your plugin's index.ts
declare module "@real-router/core" {
interface Router {
buildUrl(name: string, params?: Params): string;
matchUrl(url: string): State | undefined;
}
}
After this augmentation, router.buildUrl(...) will type-check without any casts.
6. Example: Plugin with Router Extension
import { getPluginApi } from "@real-router/core/api";
import type { PluginFactory } from "@real-router/core";
const myPlugin: PluginFactory = (router) => {
const api = getPluginApi(router);
// Add methods to router instance
const removeExtensions = api.extendRouter({
buildUrl: (name: string, params?: Params) => {
return window.location.origin + router.buildPath(name, params);
},
matchUrl: (url: string) => {
const path = new URL(url).pathname;
return api.matchPath(path);
},
});
return {
onTransitionSuccess(toState) {
// Extensions are available on router
console.log("URL:", router.buildUrl(toState.name, toState.params));
},
teardown() {
// Clean up extensions when plugin is removed
removeExtensions();
},
};
};
7. Comparison with Other Extension Mechanisms
| Mechanism | Purpose | Multiplicity | Can conflict? |
|---|---|---|---|
extendRouter |
Add new properties/methods to router | Multiple (per-key) | Yes (throws) |
addInterceptor |
Wrap existing router methods (FIFO pipeline) | Multiple (per-method) | No (chains) |
Use extendRouter when your plugin needs to add new capabilities to the router. Use addInterceptor when your plugin needs to modify the behavior of existing router methods.
Related pages: plugin-architecture ยท usePlugin ยท browser-plugin ยท error-codes ยท addBuildPathInterceptor