UNPKG

one

Version:

One is a new React Framework that makes Vite serve both native and web.

588 lines (587 loc) 23 kB
import { StackActions } from "@react-navigation/native"; import { Fragment, startTransition, useDeferredValue, useSyncExternalStore } from "react"; import { Platform } from "react-native-web"; import { devtoolsRegistry } from "../devtools/registry"; import { resolveHref } from "../link/href"; import { openExternalURL } from "../link/openExternalURL"; import { resolve } from "../link/path"; import { checkBlocker } from "../useBlocker"; import { assertIsReady } from "../utils/assertIsReady"; import { getLoaderPath, getPreloadCSSPath, getPreloadPath } from "../utils/cleanUrl"; import { dynamicImport } from "../utils/dynamicImport"; import { shouldLinkExternally } from "../utils/url"; import { ParamValidationError, RouteValidationError, validateParams as runValidateParams } from "../validateParams"; import { extractParamsFromState, extractPathnameFromHref, extractSearchFromHref, findRouteNodeFromState, findAllRouteNodesFromState } from "./findRouteNode"; import { getRouteInfo } from "./getRouteInfo"; import { getRoutes } from "./getRoutes"; import { setLastAction } from "./lastAction"; import { getLinking, resetLinking, setupLinking } from "./linkingConfig"; import { sortRoutes } from "./sortRoutes"; import { getQualifiedRouteComponent } from "./useScreens"; import { preloadRouteModules } from "./useViteRoutes"; import { getNavigateAction } from "./utils/getNavigateAction"; import { setClientMatches } from "../useMatches"; import { findInterceptRoute, setNavigationType, updateURLWithoutNavigation, storeInterceptState } from "./interceptRoutes"; import { setSlotState } from "../views/Navigator"; let routeNode = null, rootComponent; const protectedRouteRegistry = /* @__PURE__ */ new Map(); function registerProtectedRoutes(contextKey, protectedScreens) { protectedScreens.size === 0 ? protectedRouteRegistry.delete(contextKey) : protectedRouteRegistry.set(contextKey, protectedScreens); } function unregisterProtectedRoutes(contextKey) { protectedRouteRegistry.delete(contextKey); } function isRouteProtected(href) { const normalizedHref = href.replace(/^\/+|\/+$/g, ""); for (const [contextKey, protectedScreens] of protectedRouteRegistry) { const normalizedContextKey = contextKey.replace(/^\/+|\/+$/g, ""); if (normalizedHref.startsWith(normalizedContextKey)) { const routeName = normalizedHref.slice(normalizedContextKey.length).replace(/^\//, "").split("/")[0] || "index"; if (protectedScreens.has(routeName)) return !0; } } return !1; } let hasAttemptedToHideSplash = !1, initialState, rootState, nextState, routeInfo, splashScreenAnimationFrame, navigationRef = null, navigationRefSubscription; const rootStateSubscribers = /* @__PURE__ */ new Set(), loadingStateSubscribers = /* @__PURE__ */ new Set(), storeSubscribers = /* @__PURE__ */ new Set(); let currentMatches = [], validationState = { status: "idle" }; const validationStateSubscribers = /* @__PURE__ */ new Set(); function subscribeToValidationState(subscriber) { return validationStateSubscribers.add(subscriber), () => validationStateSubscribers.delete(subscriber); } function setValidationState(state) { validationState = state; for (const subscriber of validationStateSubscribers) subscriber(state); state.status === "error" && state.error && window.dispatchEvent( new CustomEvent("one-validation-error", { detail: { error: { message: state.error.message, name: state.error.name, stack: state.error.stack }, href: state.lastValidatedHref, timestamp: Date.now() } }) ); } function getValidationState() { return validationState; } function useValidationState() { return useSyncExternalStore( subscribeToValidationState, getValidationState, getValidationState ); } function initialize(context, ref, initialLocation) { if (cleanUpState(), routeNode = getRoutes(context, { ignoreEntryPoints: !0, platform: Platform.OS }), rootComponent = routeNode ? getQualifiedRouteComponent(routeNode) : Fragment, !routeNode && process.env.NODE_ENV === "production") throw new Error("No routes found"); if (process.env.ONE_DEBUG_ROUTER && routeNode) { const formatRouteTree = (node, indent = "", isLast = !0) => { const prefix = indent + (isLast ? "\u2514\u2500 " : "\u251C\u2500 "), childIndent = indent + (isLast ? " " : "\u2502 "), dynamicBadge = node.dynamic ? ` [${node.dynamic.map((d) => d.name).join(", ")}]` : "", typeBadge = node.type !== "layout" ? ` (${node.type})` : "", slotsBadge = node.slots?.size ? ` {@${Array.from(node.slots.keys()).join(", @")}}` : "", routeName = node.route || "/"; let line = `${prefix}${routeName}${dynamicBadge}${typeBadge}${slotsBadge}`; const visibleChildren = node.children.filter((child) => !child.internal); for (let i = 0; i < visibleChildren.length; i++) { const child = visibleChildren[i], childIsLast = i === visibleChildren.length - 1; line += ` ` + formatRouteTree(child, childIndent, childIsLast); } return line; }; if (console.info(`[one] \u{1F4CD} Route structure: ${formatRouteTree(routeNode)}`), routeNode.slots?.size) { console.info("[one] \u{1F4E6} Slots on root layout:"); for (const [slotName, slotConfig] of routeNode.slots) console.info(` @${slotName}:`, { defaultRoute: slotConfig.defaultRoute?.route, interceptRoutes: slotConfig.interceptRoutes.map((r) => ({ route: r.route, intercept: r.intercept })) }); } } navigationRef = ref, setupLinkingAndRouteInfo(initialLocation), subscribeToNavigationChanges(); } function cleanUpState() { initialState = void 0, rootState = void 0, nextState = void 0, routeInfo = void 0, resetLinking(), navigationRefSubscription?.(), rootStateSubscribers.clear(), storeSubscribers.clear(); } function setupLinkingAndRouteInfo(initialLocation) { initialState = setupLinking(routeNode, initialLocation), initialState ? (rootState = initialState, routeInfo = getRouteInfo(initialState)) : routeInfo = { unstable_globalHref: "", pathname: "", isIndex: !1, params: {}, segments: [] }; } function subscribeToNavigationChanges() { navigationRefSubscription = navigationRef.addListener("state", (data) => { let state = { ...data.data.state }; state.key && hashes[state.key] && (state.hash = hashes[state.key], delete hashes[state.key]), hasAttemptedToHideSplash || (hasAttemptedToHideSplash = !0, splashScreenAnimationFrame = requestAnimationFrame(() => { })), nextOptions && (state = { ...state, linkOptions: nextOptions }, nextOptions = null); let shouldUpdateSubscribers = nextState === state; nextState = void 0, state && state !== rootState && (updateState(state, void 0), shouldUpdateSubscribers = !0), shouldUpdateSubscribers && startTransition(() => { for (const subscriber of rootStateSubscribers) subscriber(state); }); }), startTransition(() => { updateSnapshot(); for (const subscriber of storeSubscribers) subscriber(); }); } function navigate(url, options) { return linkTo(resolveHref(url), "NAVIGATE", options); } function push(url, options) { return linkTo(resolveHref(url), "PUSH", options); } function dismiss(count) { process.env.ONE_DEBUG_ROUTER && console.info(`[one] \u{1F519} dismiss${count ? ` (${count})` : ""}`), navigationRef?.dispatch(StackActions.pop(count)); } function replace(url, options) { return linkTo(resolveHref(url), "REPLACE", options); } function setParams(params = {}) { return assertIsReady(navigationRef), navigationRef?.current?.setParams( // @ts-expect-error params ); } function dismissAll() { process.env.ONE_DEBUG_ROUTER && console.info("[one] \u{1F519} dismissAll"), navigationRef?.dispatch(StackActions.popToTop()); } function goBack() { process.env.ONE_DEBUG_ROUTER && console.info("[one] \u{1F519} goBack"), assertIsReady(navigationRef), navigationRef?.current?.goBack(); } function canGoBack() { return navigationRef.isReady() ? navigationRef?.current?.canGoBack() ?? !1 : !1; } function canDismiss() { let state = rootState; for (; state; ) { if (state.type === "stack" && state.routes.length > 1) return !0; if (state.index === void 0) return !1; state = state.routes?.[state.index]?.state; } return !1; } function getSortedRoutes() { if (!routeNode) throw new Error("No routes"); return routeNode.children.filter((route) => !route.internal).sort(sortRoutes); } function updateState(state, nextStateParam = state) { rootState = state, nextState = nextStateParam; const nextRouteInfo = getRouteInfo(state); if (!deepEqual(routeInfo, nextRouteInfo)) { if (process.env.ONE_DEBUG_ROUTER) { const from = routeInfo?.pathname || "(initial)", to = nextRouteInfo.pathname, params = Object.keys(nextRouteInfo.params || {}).length ? nextRouteInfo.params : void 0; console.info(`[one] \u{1F9ED} ${from} \u2192 ${to}`, params ? { params } : ""); } routeInfo = nextRouteInfo; } process.env.NODE_ENV === "development" && typeof window < "u" && (window.__oneDevtools = { routeInfo: nextRouteInfo, rootState: state, routeNode, getRoutes: () => routeNode?.children || [], getLoaderTimingHistory: () => devtoolsRegistry.getLoaderTimingHistory?.() ?? [], getPreloadHistory }, window.dispatchEvent(new CustomEvent("one-route-change", { detail: nextRouteInfo }))); } function subscribeToRootState(subscriber) { return rootStateSubscribers.add(subscriber), () => { rootStateSubscribers.delete(subscriber); }; } function subscribeToStore(subscriber) { return storeSubscribers.add(subscriber), () => { storeSubscribers.delete(subscriber); }; } function subscribeToLoadingState(subscriber) { return loadingStateSubscribers.add(subscriber), () => { loadingStateSubscribers.delete(subscriber); }; } function setLoadingState(state) { startTransition(() => { for (const listener of loadingStateSubscribers) listener(state); }); } let currentSnapshot = null; function updateSnapshot() { currentSnapshot = getSnapshot(); } function snapshot() { return currentSnapshot; } function getSnapshot() { return { linkTo, routeNode, rootComponent, linking: getLinking(), hasAttemptedToHideSplash, initialState, rootState, nextState, routeInfo, splashScreenAnimationFrame, navigationRef, navigationRefSubscription, rootStateSubscribers, storeSubscribers }; } function rootStateSnapshot() { return rootState; } function routeInfoSnapshot() { return routeInfo; } function useOneRouter() { const state = useSyncExternalStore(subscribeToStore, snapshot, snapshot); return useDeferredValue(state); } function syncStoreRootState() { if (!navigationRef) throw new Error("No navigationRef, possible duplicate One dep"); if (navigationRef.isReady()) { const currentState = navigationRef.getRootState(); rootState !== currentState && updateState(currentState); } } function useStoreRootState() { syncStoreRootState(); const state = useSyncExternalStore( subscribeToRootState, rootStateSnapshot, rootStateSnapshot ); return useDeferredValue(state); } function useStoreRouteInfo() { return syncStoreRootState(), useSyncExternalStore( subscribeToRootState, routeInfoSnapshot, routeInfoSnapshot ); } function cleanup() { splashScreenAnimationFrame && cancelAnimationFrame(splashScreenAnimationFrame); } const preloadingLoader = {}; async function doPreloadDev(href) { if (process.env.NODE_ENV === "development") { const startTime = performance.now(), normalizedPath = normalizeLoaderPath(href); try { const loaderJSUrl = getLoaderPath(href, !0), moduleLoadStart = performance.now(), modulePromise = dynamicImport(loaderJSUrl); if (!modulePromise) return null; const module = await modulePromise.catch(() => null), moduleLoadTime = performance.now() - moduleLoadStart; if (!module?.loader) return null; const executionStart = performance.now(), result = await module.loader(), executionTime = performance.now() - executionStart, totalTime = performance.now() - startTime; return devtoolsRegistry.recordLoaderTiming?.({ path: normalizedPath, startTime, moduleLoadTime, executionTime, totalTime, source: "preload" }), result ?? null; } catch (err) { const totalTime = performance.now() - startTime; return devtoolsRegistry.recordLoaderTiming?.({ path: normalizedPath, startTime, totalTime, error: err instanceof Error ? err.message : String(err), source: "preload" }), process.env.ONE_DEBUG_ROUTER && console.warn(`[one] dev preload failed for ${href}:`, err), null; } } } async function doPreload(href) { const preloadPath = getPreloadPath(href), loaderPath = getLoaderPath(href), cssPreloadPath = getPreloadCSSPath(href); recordPreloadStart(href); try { const [_preload, cssPreloadModule, loader] = await Promise.all([ dynamicImport(preloadPath), dynamicImport(cssPreloadPath)?.catch(() => null) ?? Promise.resolve(null), // graceful fail if no CSS preload dynamicImport(loaderPath)?.catch(() => null) ?? Promise.resolve(null), // graceful fail if no loader file preloadRouteModules(href) ]), hasCss = !!cssPreloadModule?.injectCSS; if (hasCss && (cssInjectFunctions[href] = cssPreloadModule.injectCSS), !!!loader?.loader) return recordPreloadComplete(href, !1, hasCss), null; const result = await loader.loader(); return recordPreloadComplete(href, !0, hasCss), result ?? null; } catch (err) { const errorMessage = err instanceof Error ? err.message : String(err); return console.error(`[one] preload error for ${href}:`, err), recordPreloadError(href, errorMessage), null; } } const preloadedLoaderData = {}, cssInjectFunctions = {}, preloadHistory = [], MAX_PRELOAD_HISTORY = 30; function recordPreloadStart(href) { if (process.env.NODE_ENV !== "development") return; const existing = preloadHistory.find((p) => p.href === href); if (existing) { existing.status = "loading", existing.startTime = performance.now(); return; } preloadHistory.unshift({ href, status: "loading", startTime: performance.now(), hasLoader: !1, hasCss: !1 }), preloadHistory.length > MAX_PRELOAD_HISTORY && preloadHistory.pop(), dispatchPreloadEvent(); } function recordPreloadComplete(href, hasLoader, hasCss) { if (process.env.NODE_ENV !== "development") return; const entry = preloadHistory.find((p) => p.href === href); entry && (entry.status = "loaded", entry.endTime = performance.now(), entry.hasLoader = hasLoader, entry.hasCss = hasCss), dispatchPreloadEvent(); } function recordPreloadError(href, error) { if (process.env.NODE_ENV !== "development") return; const entry = preloadHistory.find((p) => p.href === href); entry && (entry.status = "error", entry.endTime = performance.now(), entry.error = error), dispatchPreloadEvent(); } function dispatchPreloadEvent() { window.dispatchEvent(new CustomEvent("one-preload-update")); } function getPreloadHistory() { return preloadHistory; } function preloadRoute(href, injectCSS = !1) { if (process.env.NODE_ENV === "development") { const normalizedHref = normalizeLoaderPath(href); return preloadingLoader[normalizedHref] || (preloadingLoader[normalizedHref] = doPreloadDev(href).then((data) => (preloadedLoaderData[normalizedHref] = data, data))), preloadingLoader[normalizedHref]; } return preloadingLoader[href] || (preloadingLoader[href] = doPreload(href).then((data) => (preloadedLoaderData[href] = data, data))), injectCSS ? preloadingLoader[href]?.then(async (data) => { const inject = cssInjectFunctions[href]; return inject && await Promise.race([inject(), new Promise((r) => setTimeout(r, 800))]), data; }) : preloadingLoader[href]; } function normalizeLoaderPath(href) { return new URL(href, "http://example.com").pathname.replace(/\/index$/, "").replace(/\/$/, "") || "/"; } function buildClientMatches(href, matchingNode, params, loaderData) { const pathname = extractPathnameFromHref(href), routeId = matchingNode?.contextKey || pathname, layoutMatches = currentMatches.filter((m) => m.routeId.includes("_layout")), pageMatch = { routeId, pathname, params, loaderData }; return [...layoutMatches, pageMatch]; } function initClientMatches(matches) { currentMatches = matches, setClientMatches(matches); } async function linkTo(href, event, options) { if (process.env.ONE_DEBUG_ROUTER && console.info(`[one] \u{1F517} ${event || "NAVIGATE"} ${href}`), setNavigationType("soft"), href[0] === "#") return; if (shouldLinkExternally(href)) { openExternalURL(href); return; } if (checkBlocker(href, event === "REPLACE" ? "replace" : "push") || isRouteProtected(href)) return; const currentLayoutNode = routeNode, currentPath = routeInfo?.pathname || "/", interceptResult = findInterceptRoute(href, currentLayoutNode, currentPath); if (interceptResult) { const { interceptRoute, slotName, layoutContextKey, params: params2 } = interceptResult, scopedSlotKey = `${layoutContextKey}:${slotName}`; storeInterceptState(scopedSlotKey, interceptRoute, params2), updateURLWithoutNavigation(href), setSlotState(scopedSlotKey, { activeRouteKey: interceptRoute.contextKey, activeRouteNode: interceptRoute, params: params2, isIntercepted: !0 }); return; } assertIsReady(navigationRef); const current = navigationRef.current; if (current == null) throw new Error( "Couldn't find a navigation object. Is your component inside NavigationContainer?" ); const linking = getLinking(); if (!linking) throw new Error("Attempted to link to route when no routes are present"); if (setLastAction(), href === ".." || href === "../") { current.goBack(); return; } if (href.startsWith(".")) { let base = routeInfo?.segments?.map((segment) => { if (!segment.startsWith("[")) return segment; if (segment.startsWith("[...")) { segment = segment.slice(4, -1); const params2 = routeInfo?.params?.[segment]; return Array.isArray(params2) ? params2.join("/") : params2?.split(",")?.join("/") ?? ""; } return segment = segment.slice(1, -1), routeInfo?.params?.[segment]; }).filter(Boolean).join("/") ?? "/"; routeInfo?.isIndex || (base += "/.."), href = resolve(base, href); } const state = linking.getStateFromPath(href, linking.config); if (!state || state.routes.length === 0) { console.error( "Could not generate a valid navigation state for the given path: " + href ), console.error("linking.config", linking.config), console.error("routes", getSortedRoutes()); return; } setLoadingState("loading"), await preloadRoute(href, !0); const matchingRouteNode = findRouteNodeFromState(state, routeNode); if (matchingRouteNode?.loadRoute) { setValidationState({ status: "validating", lastValidatedHref: href }); try { const loadedRoute = matchingRouteNode.loadRoute(), params2 = extractParamsFromState(state), search = extractSearchFromHref(href), pathname = extractPathnameFromHref(href); if (loadedRoute.validateParams && runValidateParams(loadedRoute.validateParams, params2), loadedRoute.validateRoute) { const validationResult = await loadedRoute.validateRoute({ params: params2, search, pathname, href }); if (validationResult && !validationResult.valid) { const error = new RouteValidationError( validationResult.error || "Route validation failed", validationResult.details ); throw setValidationState({ status: "error", error, lastValidatedHref: href }), error; } } setValidationState({ status: "valid", lastValidatedHref: href }); } catch (error) { if (error && typeof error.then == "function") await error.catch(() => { }), setValidationState({ status: "valid", lastValidatedHref: href }); else throw (error instanceof ParamValidationError || error instanceof RouteValidationError) && setValidationState({ status: "error", error, lastValidatedHref: href }), error; } } const normalizedPath = normalizeLoaderPath(href), loaderData = preloadedLoaderData[normalizedPath], params = extractParamsFromState(state), newMatches = buildClientMatches(href, matchingRouteNode, params, loaderData); currentMatches = newMatches, setClientMatches(newMatches); const rootState2 = navigationRef.getRootState(), hash = href.indexOf("#"); rootState2.key && hash > 0 && (hashes[rootState2.key] = href.slice(hash)), nextOptions = options ?? null, startTransition(() => { const action = getNavigateAction(state, rootState2, event), current2 = navigationRef.getCurrentRoute(); navigationRef.dispatch(action); let warningTm; const interval = setInterval(() => { const next = navigationRef.getCurrentRoute(); current2 !== next && setTimeout(() => { setLoadingState("loaded"); }), clearTimeout(warningTm), clearTimeout(interval); }, 16); process.env.NODE_ENV === "development" && (warningTm = setTimeout(() => { console.warn("Routing took more than 8 seconds"); }, 1e3)); }); } const hashes = {}; let nextOptions = null; function deepEqual(a, b) { if (a === b) return !0; if (Array.isArray(a) && Array.isArray(b)) { if (a.length !== b.length) return !1; for (let i = 0; i < a.length; i++) if (!deepEqual(a[i], b[i])) return !1; return !0; } if (typeof a == "object" && typeof b == "object") { const keysA = Object.keys(a), keysB = Object.keys(b); if (keysA.length !== keysB.length) return !1; for (const key of keysA) if (!deepEqual(a[key], b[key])) return !1; return !0; } return !1; } export { canDismiss, canGoBack, cleanup, dismiss, dismissAll, getPreloadHistory, getSortedRoutes, getValidationState, goBack, hasAttemptedToHideSplash, initClientMatches, initialState, initialize, isRouteProtected, linkTo, navigate, navigationRef, preloadRoute, preloadedLoaderData, preloadingLoader, push, registerProtectedRoutes, replace, rootComponent, rootState, rootStateSnapshot, routeInfo, routeInfoSnapshot, routeNode, setLoadingState, setParams, setValidationState, snapshot, subscribeToLoadingState, subscribeToRootState, subscribeToStore, subscribeToValidationState, unregisterProtectedRoutes, updateState, useOneRouter, useStoreRootState, useStoreRouteInfo, useValidationState }; //# sourceMappingURL=router.js.map