one
Version:
One is a new React Framework that makes Vite serve both native and web.
376 lines (373 loc) • 13.7 kB
JavaScript
import { createNavigatorFactory } from "@react-navigation/core";
import { SafeAreaProviderCompat } from "@react-navigation/elements";
import React, { memo, Suspense, 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 { 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 { 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 !1;
const checkElement = child => {
if (!React.isValidElement(child)) return !1;
if (child.type === "meta") {
const props = child.props;
if ("charSet" in props || "charset" in props) return !0;
}
const childProps = child.props;
return childProps.children ? hasMetaCharset(childProps.children) : !1;
};
return Array.isArray(children) ? children.some(checkElement) : checkElement(children);
}
return !0;
}
const {
Screen,
Group
} = createNavigatorFactory({})(),
cachedInlineCSSElements = typeof document < "u" ? Array.from(document.querySelectorAll('style[id^="__one_css_"]')).map((el, i) => /* @__PURE__ */jsx("style", {
id: el.id,
dangerouslySetInnerHTML: {
__html: el.innerHTML
}
}, `inline-css-${i}`)) : [];
function RootLayoutRenderer({
LayoutComponent,
layoutProps,
forwardedRef
}) {
if (process.env.NODE_ENV === "development") {
const [, setHmrKey] = useState(0);
useEffect(() => {
const handler = () => setHmrKey(k => k + 1);
return window.addEventListener("one-hmr-update", handler), () => window.removeEventListener("one-hmr-update", handler);
}, []);
}
const out = LayoutComponent(layoutProps, forwardedRef),
{
children,
bodyProps,
head,
htmlProps
} = filterRootHTML(out),
{
children: headChildren,
...headProps
} = head?.props || {},
serverContext = useServerContext();
let finalChildren = children;
return process.env.NODE_ENV === "development" && (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 ?
// Inline CSS: SSR renders fresh, client uses cached elements from module load
serverContext?.cssContents ? serverContext.cssContents.map((content, i) => /* @__PURE__ */jsx("style", {
id: `__one_css_${i}`,
dangerouslySetInnerHTML: {
__html: content
}
}, `inline-css-${i}`)) : cachedInlineCSSElements : serverContext?.css?.map(file => /* @__PURE__ */jsx("link", {
rel: "stylesheet",
href: file
}, file)), /* @__PURE__ */jsx(ServerContextScript, {}), headChildren]
}, "head"), /* @__PURE__ */jsx("body", {
suppressHydrationWarning: !0,
...bodyProps,
children: /* @__PURE__ */jsx(SafeAreaProviderCompat, {
children: finalChildren
})
}, "body")]
}),
// tamagui and libraries can add className on hydration to have ssr safe styling
// so supress hydration warnings here
/* @__PURE__ */
jsx("html", {
suppressHydrationWarning: !0,
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],
ordered = order.map(({
name,
redirect,
initialParams,
listeners,
options: options2,
getId
}) => {
if (!entries.length) return console.warn(`[Layout children]: Too many screens defined. Route "${name}" is extraneous.`), null;
const matchIndex = entries.findIndex(child => child.route === name);
if (matchIndex === -1) return console.warn(`[Layout children]: No route named "${name}" exists in nested children:`, children.map(({
route
}) => route)), null;
const match = entries[matchIndex];
if (entries.splice(matchIndex, 1), 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);
return options?.onlyMatching || ordered.push(...entries.sort(sortRoutesWithInitial(initialRouteName)).map(route => ({
route,
props: {}
}))), ordered;
}
function useSortedScreens(order, options) {
const node = useRouteNode();
return React.useMemo(() => (node?.children?.length ? getSortedChildren(node.children, order, node.initialRouteName, options) : []).filter(value => !options?.protectedScreens?.has(value.route.route)).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) => {
const children = React.createElement(getPageExport(component) || EmptyRoute, {
...props,
ref
});
return /* @__PURE__ */jsx(Try, {
catch: ErrorBoundary,
children
});
})
};
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") {
const [, setHmrKey] = useState(0);
useEffect(() => {
const handler = () => setHmrKey(k => k + 1);
return window.addEventListener("one-hmr-update", handler), () => window.removeEventListener("one-hmr-update", handler);
}, []);
}
const res = value.loadRoute(),
Component = getPageExport(fromImport(res));
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
});
return props.segment === "" ? /* @__PURE__ */jsx(RootLayoutRenderer, {
LayoutComponent: Component,
layoutProps: {
...props,
...slotProps
},
forwardedRef: ref
}) : /* @__PURE__ */jsx(RouteErrorBoundary, {
routeName: value.route,
children: /* @__PURE__ */jsx(Component, {
...props,
...slotProps,
ref
})
});
}),
wrapSuspense = children => process.env.ONE_SUSPEND_ROUTES === "1" ? /* @__PURE__ */jsx(Suspense, {
fallback: null,
children
}) : children,
QualifiedRoute = React.forwardRef(({
// Remove these React Navigation props to
// enforce usage of router hooks (where the query params are correct).
route,
navigation,
// Pass all other props to the component
...props
}, ref) => /* @__PURE__ */jsx(Route, {
route,
node: value,
children: /* @__PURE__ */jsx(Fragment, {
children: wrapSuspense(/* @__PURE__ */jsx(ScreenComponent, {
...props,
ref,
// Expose the template segment path, e.g. `(home)`, `[foo]`, `index`
// the intention is to make it possible to deduce shared routes.
segment: value.route
}))
})
}));
return QualifiedRoute.displayName = `Route(${value.route})`, qualifiedStore.set(value, QualifiedRoute), 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];
Array.isArray(value) && value.length > 0 ? segments.push(value.join("/")) : value && !Array.isArray(value) ? segments.push(value) : dynamic.deep ? segments.push(`[...${dynamic.name}]`) : 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,
staticResult = typeof staticOptions == "function" ? staticOptions(args) : staticOptions,
dynamicResult = typeof options == "function" ? options?.(args) : options,
output = {
...staticResult,
...dynamicResult
};
return route.generated && (output.tabBarButton = () => null, output.drawerItemStyle = {
height: 0,
display: "none"
}), output;
},
getComponent: () => getQualifiedRouteComponent(route)
});
}
const ROUTE_ERROR_BOUNDARY_INITIAL_STATE = {
hasError: !1,
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: !0,
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
});
}
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