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

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