UNPKG

react-best-router

Version:

In larger React applications, it is usually necessary to use a Router to handle relationships between pages and navigate between them. Are you using other Router libraries? Have you encountered any areas that are not very useful? For example, defining nes

340 lines (324 loc) 12.8 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var react = require('react'); const RouterContext = react.createContext(null); function useRouterContext() { const value = react.useContext(RouterContext); if (!value) throw "Invalid call outside of Router"; return value; } function normalizePath(path) { const segments = path.split("/").map((it) => (it || "").trim()); const len = segments.length; const normalizedPath = segments .reduce((result, it, i) => { if (it === "." || (it === "" && i !== 0 && i !== len - 1)) return result; return it === ".." ? result.slice(0, -1) : [...result, it]; }, []) .join("/"); return normalizedPath.length > 1 && normalizedPath.at(-1) === "/" ? normalizedPath.slice(0, -1) : normalizedPath; } function resolvePath(from, to) { return normalizePath(`${from}/${to}`); } function patternToRegExp(pattern) { const table = []; let id = 0; let text = pattern; text = text.replace(/\\\(/, "@@#").replace(/\\\)/, "#@@"); const named = /(?<S>\/)?:(?<N>[a-z0-9_]+)(?<R>\(.+?\))?(?<M>[*?+])?/gi; text = text.replace(named, function (_, S = "", N = "", R = "", M = "") { const value = M === "*" || M === "+" ? `(?<${N}>${R || `(${S}[a-z0-9_-]+)`}${M})` : `${S}${M}(?<${N}>${R || "([a-z0-9_-]+)"}${M})`; const key = `@@${id++}@@`; table.push([key, value]); return key; }); const anonymous = /(?<S>\/)?(?<R>\(.+?\))(?<M>[*?+])?/gi; let index = 0; text = text.replace(anonymous, function (_, S = "", R = "", M = "") { const value = M === "*" || M === "+" ? `(?<$${++index}>${R}${M})` : `${S}${M}(?<$${++index}>${R}${M})`; const key = `@@${id++}@@`; table.push([key, value]); return key; }); text = text .replace(/\?/g, "\\?") .replace(/\*/g, "\\*") .replace(/\+/g, "\\+") .replace(/\./g, "\\.") .replace(/\//g, "\\/"); table.forEach(([key, value]) => (text = text.replace(key, value))); text = text.replace(/@@#/g, "(").replace(/#@@/g, ")"); try { return new RegExp(`^${text}$`, "i"); } catch (_a) { throw `Invalid pattern: ${pattern}`; } } function patternToPrefix(pattern) { const expr = /[.*()[\]?+]/; const segments = pattern.split("/"); const lastIndex = segments .slice(0) .reverse() .findIndex((it) => !expr.test(it)); return (lastIndex > 0 ? segments.slice(0, -lastIndex) : segments).join("/"); } function getMatcherFullPrefix(matcher) { let current = matcher; let fullPrefix = ""; while (current) { fullPrefix = `${current.prefix}/${fullPrefix}`; current = current.parent; } return fullPrefix; } function normalizePattern(normalizedPattern) { normalizedPattern = normalizePath(normalizedPattern); return normalizedPattern.length > 1 && normalizedPattern.at(-1) === "/" ? normalizedPattern.slice(0, -1) : normalizedPattern; } function patternToMatch(pattern) { const regexp = patternToRegExp(pattern); return (path) => { const { pathname, searchParams } = new URL(path, location.origin); const info = regexp === null || regexp === void 0 ? void 0 : regexp.exec(decodeURIComponent(pathname)); return { state: !!info, params: (info === null || info === void 0 ? void 0 : info.groups) || {}, query: searchParams, }; }; } function createMatcher(pattern, prefix, parent) { if (parent && parent.prefix === parent.pattern) { const err = `Invalid nesting '${parent.pattern} -> ${pattern}'`; throw new Error(err); } prefix = prefix || patternToPrefix(pattern); const fullPattern = `/${getMatcherFullPrefix(parent)}/${pattern}`; const match = patternToMatch(normalizePattern(fullPattern)); const matcher = { parent, pattern, prefix, match }; matcher.match = (path) => { matcher.result = match(path); return matcher.result; }; return matcher; } const MatcherContext = react.createContext(null); function useParentMatcher() { return react.useContext(MatcherContext); } function toPathName(path) { const { pathname } = new URL(path, location.origin); return pathname; } function toFullPath(base, from, to) { return to.startsWith("/") ? normalizePath(`${toPathName(base)}/${to}`) : resolvePath(toPathName(from), to); } function toParsedPath(base, path) { return normalizePath(`/${path.slice(base.length)}`).split("?")[0]; } function useNavigator(paramsParser) { const { base, state, driver } = useRouterContext(); const matcher = useParentMatcher(); return react.useMemo(() => { if (!(matcher === null || matcher === void 0 ? void 0 : matcher.result)) throw "Invalid call outside of Route"; const push = (to) => driver.push({ path: toFullPath(base, state.path, to) }); const replace = (to) => driver.replace({ path: toFullPath(base, state.path, to) }); const back = () => driver.back(); const forward = () => driver.forward(); const go = (step) => driver.go(step); const path = toParsedPath(base, state.path); const { params, query } = matcher.result; if (paramsParser && params) { Object.entries(params).forEach(([key, value]) => { const parse = paramsParser[key]; params[key] = parse ? parse(String(value)) : value; }); } return { path, params, query, push, back, forward, go, replace }; }, [state, matcher, driver]); } function NavigatorForwarder({ navRef }) { const { parser } = navRef || {}; const nav = useNavigator(parser); react.useMemo(() => navRef && (navRef.current = nav), [nav, navRef]); return react.createElement(react.Fragment); } function useNavigatorRef(paramsParser) { return react.useMemo(() => { return createNavigatorRef(paramsParser); }, [paramsParser]); } function createNavigatorRef(paramsParser) { return { parser: paramsParser, current: void 0 }; } function takeSidePatterns(type, elements) { const validItems = react.Children.toArray(elements).filter((it) => !!it); const routeItems = validItems.filter((it) => it.type === type); return validItems.length === routeItems.length ? routeItems.map((it) => { var _a; return (_a = it.props) === null || _a === void 0 ? void 0 : _a.pattern; }) : []; } function RouteFallback(props) { const { side, children } = props; const patterns = takeSidePatterns(...side); const { state } = useRouterContext(); const parentMatcher = useParentMatcher(); const matchers = react.useMemo(() => patterns.map((it) => createMatcher(it, "", parentMatcher)), [patterns.join(":"), state.path, parentMatcher]); return !matchers[0] || matchers.some((it) => it.match(state.path).state) ? (react.createElement(react.Fragment, null)) : (children); } function RouteRedirect(props) { const nav = useNavigator(); const { to } = props; react.useEffect(() => nav.push(to), [nav, to]); return react.createElement(react.Fragment, null); } function Route(props) { const { pattern = "/(.*)?", prefix, navigator } = props; const { render, children, fallback, redirect } = props; const { state } = useRouterContext(); const parentMatcher = useParentMatcher(); const matcher = react.useMemo(() => createMatcher(pattern, prefix, parentMatcher), [pattern, prefix, parentMatcher]); const matched = matcher.match(state.path).state; if (!matched && !fallback) return react.createElement(react.Fragment, null); if (matched && redirect) return react.createElement(RouteRedirect, { to: redirect }); const elements = render ? render(children) : children; return (react.createElement(MatcherContext.Provider, { value: matcher }, navigator && react.createElement(NavigatorForwarder, { navRef: navigator }), matched ? elements : fallback, matched && fallback && (react.createElement(RouteFallback, { side: [Route, elements] }, fallback)))); } function normalizeState(state) { return { ...state, path: normalizePath(state.path) }; } function Router(props) { const { driver, base = "/", navigator, children, render, fallback } = props; const [state, setState] = react.useState(() => { return normalizeState(driver.current()); }); const context = react.useMemo(() => ({ base, state, driver }), [base, state, driver]); react.useLayoutEffect(() => driver.subscribe((state) => setState(normalizeState(state))), [driver, state]); return (react.createElement(RouterContext.Provider, { value: context }, react.createElement(Route, { pattern: `${base}/(.*)?`, prefix: base, render: render, navigator: navigator, fallback: fallback }, children))); } function createBrowserDriver() { const current = () => { const { pathname, search } = location; return { path: `${pathname}${search}` }; }; const push = (state) => { history.pushState(state, state.path, state.path); const event = new PopStateEvent("popstate", { state }); window.dispatchEvent(event); }; const replace = (state) => { history.replaceState(state, state.path, state.path); const event = new PopStateEvent("popstate", { state }); window.dispatchEvent(event); }; const go = (step) => history.go(step); const back = () => history.back(); const forward = () => history.forward(); const subscribe = (handler) => { const popstateHandler = (event) => { handler(event.state || current()); }; window.addEventListener("popstate", popstateHandler); return () => window.removeEventListener("popstate", popstateHandler); }; return { current, push, replace, go, back, forward, subscribe }; } function useBrowserDriver() { return react.useMemo(() => createBrowserDriver(), []); } function createHashDriver() { const current = () => { return { path: location.hash.slice(1) || "/" }; }; const push = (state) => { location.hash = state.path; }; const replace = (state) => { location.hash = state.path; }; const go = (step) => history.go(step); const back = () => history.back(); const forward = () => history.forward(); const subscribe = (handler) => { const hashChangeHandler = () => handler(current()); window.addEventListener("hashchange", hashChangeHandler); return () => window.removeEventListener("hashchange", hashChangeHandler); }; return { current, push, replace, go, back, forward, subscribe }; } function useHashDriver() { return react.useMemo(() => createHashDriver(), []); } function createMemoryDriver(initialState = { path: "/" }) { let handler; let stack = [initialState]; let cursor = 0; const setCursor = (index, force = false) => { if (index < 0) index = 0; if (index > stack.length - 1) index = stack.length - 1; if (index === cursor && !force) return; cursor = index; handler === null || handler === void 0 ? void 0 : handler(stack[cursor] || initialState); }; return { subscribe: (callback) => { handler = callback; return () => (handler = undefined); }, current: () => stack[cursor] || initialState, replace: (state) => { stack = stack.slice(0, cursor + 1); stack[cursor] = state; setCursor(stack.length - 1, true); }, push: (state) => { stack = stack.slice(0, cursor + 1); stack.push(state); setCursor(stack.length - 1); }, go: (step) => setCursor(cursor + step), back: () => setCursor(cursor - 1), forward: () => setCursor(cursor + 1), }; } function useMemoryDriver(initialState = { path: "/" }) { return react.useMemo(() => createMemoryDriver(initialState), [initialState]); } exports.NavigatorForwarder = NavigatorForwarder; exports.Route = Route; exports.Router = Router; exports.createBrowserDriver = createBrowserDriver; exports.createHashDriver = createHashDriver; exports.createMemoryDriver = createMemoryDriver; exports.createNavigatorRef = createNavigatorRef; exports.normalizeState = normalizeState; exports.useBrowserDriver = useBrowserDriver; exports.useHashDriver = useHashDriver; exports.useMemoryDriver = useMemoryDriver; exports.useNavigator = useNavigator; exports.useNavigatorRef = useNavigatorRef;