UNPKG

one

Version:

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

430 lines (427 loc) 14.7 kB
import { createNavigatorFactory } from "@react-navigation/core"; import { SafeAreaProviderCompat } from "@react-navigation/elements"; import React, { memo, Suspense, useContext, useEffect, useState } from "react"; import { SafeAreaView, ScrollView, Text, TouchableOpacity, View } from "react-native-web"; import { ServerContextScript } from "../server/ServerContextScript.mjs"; import { getPageExport } from "../utils/getPageExport.mjs"; import { EmptyRoute } from "../views/EmptyRoute.mjs"; import { Try } from "../views/Try.mjs"; import { checkSkewAndReload } from "../skewProtection.mjs"; import { handleSkewError, isChunkLoadError } from "../utils/dynamicImport.mjs"; import { DevHead } from "../vite/DevHead.mjs"; import { useServerContext } from "../vite/one-server-only.mjs"; import { filterRootHTML } from "./filterRootHTML.mjs"; import { Route, useRouteNode } from "./Route.mjs"; import { SpaShellContext } from "./SpaShellContext.mjs"; import { NamedSlot } from "../views/Navigator.mjs"; import { sortRoutesWithInitial } from "./sortRoutes.mjs"; import { Fragment, jsx, jsxs } from "react/jsx-runtime"; import { createElement } from "react"; function hasMetaCharset(children) { if (process.env.NODE_ENV === "development") { if (!children) return false; const checkElement = child => { if (!React.isValidElement(child)) return false; if (child.type === "meta") { const props = child.props; if ("charSet" in props || "charset" in props) return true; } const childProps = child.props; if (childProps.children) return hasMetaCharset(childProps.children); return false; }; if (Array.isArray(children)) return children.some(checkElement); return checkElement(children); } return true; } const { Screen, Group } = createNavigatorFactory({})(); const cachedInlineCSSElements = typeof document !== "undefined" ? (() => { const elements = []; document.querySelectorAll("style[id^=\"__one_css_\"], link[rel=\"stylesheet\"][data-one-css]").forEach((el, i) => { if (el.tagName === "STYLE") elements.push(/* @__PURE__ */jsx("style", { id: el.id, dangerouslySetInnerHTML: { __html: el.innerHTML } }, `inline-css-${i}`));else { const href = el.getAttribute("href"); elements.push(/* @__PURE__ */jsx("link", { rel: "stylesheet", href, "data-one-css": "" }, href)); } }); return elements; })() : []; function RootLayoutRenderer({ LayoutComponent, layoutProps, forwardedRef }) { if (process.env.NODE_ENV === "development" && true) { const [, setHmrKey] = useState(0); useEffect(() => { const handler = () => setHmrKey(k => k + 1); window.addEventListener("one-hmr-update", handler); return () => window.removeEventListener("one-hmr-update", handler); }, []); } const { children, bodyProps, head, htmlProps } = filterRootHTML(LayoutComponent(layoutProps, forwardedRef)); const { children: headChildren, ...headProps } = head?.props || {}; const serverContext = useServerContext(); let finalChildren = children; if (process.env.NODE_ENV === "development") { if (!hasMetaCharset(headChildren)) console.warn(`[one] Missing <meta charSet="utf-8" /> in your root _layout.tsx <head>. This can cause React hydration issues due to encoding mismatch. Add it as the first element in your <head> tag.`); } finalChildren = /* @__PURE__ */jsxs(Fragment, { children: [/* @__PURE__ */jsxs("head", { ...headProps, children: [/* @__PURE__ */jsx(DevHead, {}), /* @__PURE__ */jsx("script", { dangerouslySetInnerHTML: { __html: `globalThis['global'] = globalThis` } }), serverContext?.cssContents?.length || serverContext?.cssInlineCount ? serverContext?.cssContents ? serverContext.cssContents.map((content, i) => content ? /* @__PURE__ */jsx("style", { id: `__one_css_${i}`, dangerouslySetInnerHTML: { __html: content } }, `inline-css-${i}`) : serverContext.css?.[i] ? /* @__PURE__ */jsx("link", { rel: "stylesheet", href: serverContext.css[i] }, serverContext.css[i]) : null) : cachedInlineCSSElements : serverContext?.css?.map(file => /* @__PURE__ */jsx("link", { rel: "stylesheet", href: file }, file)), /* @__PURE__ */jsx(ServerContextScript, {}), headChildren] }, "head"), /* @__PURE__ */jsx("body", { suppressHydrationWarning: true, ...bodyProps, children: /* @__PURE__ */jsx(SafeAreaProviderCompat, { children: finalChildren }) }, "body")] }); return /* @__PURE__ */jsx("html", { suppressHydrationWarning: true, lang: "en-US", ...htmlProps, children: finalChildren }); } function getSortedChildren(children, order, initialRouteName, options) { if (!order?.length) return children.sort(sortRoutesWithInitial(initialRouteName)).map(route => ({ route, props: {} })); const entries = [...children]; const ordered = order.map(({ name, redirect, initialParams, listeners, options: options2, getId }) => { if (!entries.length) { console.warn(`[Layout children]: Too many screens defined. Route "${name}" is extraneous.`); return null; } const matchIndex = entries.findIndex(child => child.route === name); if (matchIndex === -1) { console.warn(`[Layout children]: No route named "${name}" exists in nested children:`, children.map(({ route }) => route)); return null; } const match = entries[matchIndex]; entries.splice(matchIndex, 1); if (redirect) { if (typeof redirect === "string") throw new Error(`Redirecting to a specific route is not supported yet.`); return null; } return { route: match, props: { initialParams, listeners, options: options2, getId } }; }).filter(Boolean); if (!options?.onlyMatching) ordered.push(...entries.sort(sortRoutesWithInitial(initialRouteName)).map(route => ({ route, props: {} }))); return ordered; } function useSortedScreens(order, options) { const node = useRouteNode(); return React.useMemo(() => { return (node?.children?.length ? getSortedChildren(node.children, order, node.initialRouteName, options) : []).filter(value => { const routeName = value.route.route; const normalized = routeName.replace(/\/index$/, ""); return !(options?.protectedScreens?.has(routeName) || options?.protectedScreens?.has(normalized)); }).map(value => routeToScreen(value.route, value.props)); }, [node?.children, node?.initialRouteName, order, options?.protectedScreens]); } function fromImport({ ErrorBoundary, ...component }) { if (ErrorBoundary) return { default: React.forwardRef((props, ref) => { return /* @__PURE__ */jsx(Try, { catch: ErrorBoundary, children: React.createElement(getPageExport(component) || EmptyRoute, { ...props, ref }) }); }) }; if (process.env.NODE_ENV !== "production") { const exported = getPageExport(component); if (exported && typeof exported === "object" && Object.keys(exported).length === 0) return { default: EmptyRoute }; } return { default: getPageExport(component) }; } const qualifiedStore = /* @__PURE__ */new WeakMap(); function getQualifiedRouteComponent(value) { if (value && qualifiedStore.has(value)) return qualifiedStore.get(value); const ScreenComponent = React.forwardRef((props, ref) => { if (process.env.NODE_ENV === "development" && true) { const [, setHmrKey] = useState(0); useEffect(() => { const handler = () => setHmrKey(k => k + 1); window.addEventListener("one-hmr-update", handler); return () => window.removeEventListener("one-hmr-update", handler); }, []); } if (useContext(SpaShellContext) && props.segment !== "") { if (!(value.children?.length && (value.layoutRenderMode === "ssg" || value.layoutRenderMode === "ssr"))) return /* @__PURE__ */jsx("div", { "data-one-spa-content": "" }); } const Component = getPageExport(fromImport(value.loadRoute())); if (process.env.NODE_ENV === "development" && process.env.DEBUG === "one") { console.groupCollapsed(`Render ${props.key} ${props.segment}`); console.info(`value`, value); console.info(`Component`, Component); console.groupEnd(); } const slotProps = {}; if (value.slots && value.slots.size > 0) for (const [slotName] of value.slots) slotProps[slotName] = /* @__PURE__ */jsx(NamedSlot, { name: slotName, layoutContextKey: value.contextKey }); if (props.segment === "") return /* @__PURE__ */jsx(RootLayoutRenderer, { LayoutComponent: Component, layoutProps: { ...props, ...slotProps }, forwardedRef: ref }); return /* @__PURE__ */jsx(RouteErrorBoundary, { routeName: value.route, children: /* @__PURE__ */jsx(Component, { ...props, ...slotProps, ref }) }); }); const wrapSuspense = children => { if (process.env.ONE_SUSPEND_ROUTES === "1" && globalThis.__ONE_DISABLE_SUSPENSE_ROUTES__ !== true) return /* @__PURE__ */jsx(Suspense, { fallback: null, children }); return children; }; const QualifiedRoute = React.forwardRef(({ route, navigation, ...props }, ref) => { return /* @__PURE__ */jsx(Route, { route, node: value, children: /* @__PURE__ */jsx(Fragment, { children: wrapSuspense(/* @__PURE__ */jsx(ScreenComponent, { ...props, ref, segment: value.route })) }) }); }); QualifiedRoute.displayName = `Route(${value.route})`; qualifiedStore.set(value, QualifiedRoute); return memo(QualifiedRoute); } function createGetIdForRoute(route) { const include = /* @__PURE__ */new Map(); if (route.dynamic) for (const segment of route.dynamic) include.set(segment.name, segment); return ({ params = {} } = {}) => { const segments = []; for (const dynamic of include.values()) { const value = params?.[dynamic.name]; if (Array.isArray(value) && value.length > 0) segments.push(value.join("/"));else if (value && !Array.isArray(value)) segments.push(value);else if (dynamic.deep) segments.push(`[...${dynamic.name}]`);else segments.push(`[${dynamic.name}]`); } return segments.join("/") ?? route.contextKey; }; } function routeToScreen(route, { options, ...props } = {}) { return /* @__PURE__ */createElement(Screen, { getId: createGetIdForRoute(route), ...props, name: route.route, key: route.route, options: args => { const staticOptions = route.generated ? route.loadRoute()?.getNavOptions : null; const staticResult = typeof staticOptions === "function" ? staticOptions(args) : staticOptions; const dynamicResult = typeof options === "function" ? options?.(args) : options; const output = { ...staticResult, ...dynamicResult }; if (route.generated) { output.tabBarButton = () => null; output.drawerItemStyle = { height: 0, display: "none" }; } return output; }, getComponent: () => { return getQualifiedRouteComponent(route); } }); } const ROUTE_ERROR_BOUNDARY_INITIAL_STATE = { hasError: false, error: null, errorInfo: null }; class RouteErrorBoundary extends React.Component { constructor(props) { super(props); this.state = ROUTE_ERROR_BOUNDARY_INITIAL_STATE; } static getDerivedStateFromError(error) { return { hasError: true, error }; } componentDidCatch(error, errorInfo) { console.error(`Error occurred while running route "${this.props.routeName}": ${error instanceof Error ? error.message : error} ${error.stack} Component Stack: ${errorInfo.componentStack}`); this.setState({ errorInfo }); if (process.env.NODE_ENV === "production" && process.env.ONE_SKEW_PROTECTION !== "false") if (isChunkLoadError(error)) handleSkewError();else checkSkewAndReload(); } clearError() { this.setState(ROUTE_ERROR_BOUNDARY_INITIAL_STATE); } render() { if (this.state.hasError) { const { error, errorInfo } = this.state; return /* @__PURE__ */jsx(SafeAreaView, { style: { backgroundColor: "#000" }, children: /* @__PURE__ */jsxs(View, { style: { margin: 16, gap: 16 }, children: [/* @__PURE__ */jsxs(Text, { style: { alignSelf: "flex-start", padding: 5, margin: -5, backgroundColor: "red", color: "white", fontSize: 20, fontFamily: "monospace" }, children: ["Error on route \"", this.props.routeName, "\""] }), /* @__PURE__ */jsx(Text, { style: { color: "white", fontSize: 16, fontFamily: "monospace" }, children: error instanceof Error ? error.message : error }), /* @__PURE__ */jsx(TouchableOpacity, { onPress: this.clearError.bind(this), children: /* @__PURE__ */jsx(Text, { style: { alignSelf: "flex-start", margin: -6, padding: 6, backgroundColor: "white", color: "black", fontSize: 14, fontFamily: "monospace" }, children: "Retry" }) }), /* @__PURE__ */jsxs(ScrollView, { contentContainerStyle: { gap: 12 }, children: [error instanceof Error ? /* @__PURE__ */jsx(Text, { style: { color: "white", fontSize: 12, fontFamily: "monospace" }, children: error.stack }) : null, errorInfo?.componentStack ? /* @__PURE__ */jsxs(Text, { style: { color: "white", fontSize: 12, fontFamily: "monospace" }, children: ["Component Stack: ", errorInfo.componentStack] }) : null] })] }) }); } return this.props.children; } } export { Group, Screen, createGetIdForRoute, getQualifiedRouteComponent, useSortedScreens }; //# sourceMappingURL=useScreens.mjs.map