@mcpronovost/okp-router
Version:
A lightweight routing solution specifically designed for Vite-based projects with multilingual support.
198 lines (179 loc) • 6.22 kB
text/typescript
import type { RouteType, RouteModulesType } from "./types";
import { routerConfig, REGEX } from "./config";
import { getLangAndUri } from "./utils";
/**
* Get all routes
* @param modules Route modules from Vite's glob import
* @returns Object with all routes
* @since 0.1.1
*/
export const getRoutes = (
modules?: RouteModulesType | undefined
): Record<string, RouteType> => {
if (modules || routerConfig.routeModules) {
return (() => {
return Object.values(modules || routerConfig.routeModules || {}).reduce<
Record<string, RouteType>
>((acc, module) => {
const firstRoute = Object.values(module)[0] as unknown as Record<
string,
RouteType
>;
return { ...acc, ...firstRoute };
}, {});
})();
}
return routerConfig.routes;
};
/**
* Recursively finds a route by matching the URI to translations in the route map
* @param uri The URI path to match against route translations
* @param lang The language code to use for matching (e.g., "en" or "fr")
* @param routesList Optional route map to search through. Defaults to global routes if not provided
* @param parentPath Optional dot-notation path of parent routes. Used internally for recursion
* @returns A tuple containing [fullRoutePath, routeObject] if found, undefined otherwise
* @since 0.1.0
*/
export const getRoute = (
uri: string,
lang: string = routerConfig.currentLang || routerConfig.defaultLang,
routesList?: Record<string, RouteType>,
parentPath: string = ""
): [string, RouteType] => {
if (!routesList) routesList = getRoutes();
if (uri === "/") uri = "";
const params = {};
for (const [key, route] of Object.entries(routesList)) {
const fullPath = parentPath ? `${parentPath}.${key}` : key;
const routePath = route.paths[lang];
// Handle dynamic path segments
if (routePath.toString().includes("{")) {
// Extract parameter names from the route path
const paramNames = [...routePath.matchAll(REGEX.PARAM)].map(
(match) => match[1]
);
const pathPattern = routePath.replace(REGEX.PARAM, "([^/]+)");
const regex = new RegExp(`^${pathPattern}$`);
const matches = uri.match(regex);
if (matches) {
// Store captured values with their parameter names
paramNames.forEach((name, index) => {
params[name] = matches[index + 1];
});
return [fullPath, { ...route, params }];
}
} else if (routePath === uri) {
return [fullPath, { ...route, params }];
}
// Check for child routes recursively
if (route.children) {
const childUri = `${route.paths[lang]}/`;
if (
uri.startsWith(childUri) ||
(routePath.includes("{") &&
new RegExp(
`^${routePath.replace(REGEX.PARAM_REPLACE, "[^/]+")}/`
).test(uri))
) {
const nextParentPath = parentPath ? `${parentPath}.${key}` : key;
// Extract params from current level if it's a dynamic route
if (routePath.includes("{")) {
const paramNames = [...routePath.matchAll(/{([^}]+)}/g)].map(
(match) => match[1]
);
const pathPattern = routePath.replace(REGEX.PARAM_REPLACE, "([^/]+)");
const matches = uri.match(new RegExp(`^${pathPattern}/`));
if (matches) {
paramNames.forEach((name, index) => {
params[name] = matches[index + 1];
});
}
}
const childRoute = getRoute(
uri.replace(
new RegExp(`^${routePath.replace(/{[^}]+}/g, "[^/]+")}/`),
""
),
lang,
route.children,
nextParentPath
);
if (childRoute) {
// Merge params from child route with current params
return [childRoute[0], childRoute[1]];
}
}
}
}
return [
uri,
{
view: "errors/404",
paths: {},
auth: false,
props: {},
params: {},
},
];
};
/**
* Find the localized route path from a specific language view name
* @param uri The target view name
* @param toLang The target language
* @param fromLang The specific language to translate view name from
* @param additionalParams Optional additional parameters to pass to the new route
* @returns The localized route path in the target language, or the original view name if no translation is found
* @since 0.1.0
*/
export const getLocalizedRoute = (
uri: string,
toLang: string = routerConfig.currentLang || routerConfig.defaultLang,
fromLang: string = "en",
additionalParams?: Record<string, string>
): string => {
// Find the current route based on the URI and current language
const currentRoute = getRoute(uri, fromLang);
if (!currentRoute) return `/${toLang}/${uri}`;
const [routePath, routeData] = currentRoute;
const params = { ...routeData.params, ...additionalParams };
// Split the route path to handle nested routes
const routeParts = routePath.split(".");
let routesList = getRoutes();
let toPath = "";
// Build the new path by traversing the route tree
for (let i = 0; i < routeParts.length; i++) {
const part = routeParts[i];
const currentPart = routesList[part];
if (currentPart) {
toPath += (i > 0 ? "/" : "") + currentPart.paths[toLang];
routesList = currentPart.children || {};
}
}
// Replace params in the path
if (params) {
for (const [key, value] of Object.entries(params)) {
toPath = toPath.replace(`{${key}}`, value);
}
}
return `/${toLang}/${toPath}`;
};
/**
* Switch the language of the current route
* @param toLang The target language
* @param additionalParams Optional additional parameters to pass to the new route
* @returns The new route path in the target language
* @since 0.4.1
*/
export const switchRouteLanguage = (
toLang: string = routerConfig.currentLang || routerConfig.defaultLang || "en",
additionalParams?: Record<string, string>
): string => {
const { langCode, uri } = getLangAndUri(window.location.pathname);
const newRoute = getLocalizedRoute(
uri,
toLang,
langCode,
additionalParams
);
return newRoute;
};