expo-router
Version:
Expo Router is a file-based router for React Native and web applications.
246 lines • 9.47 kB
JavaScript
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Head = void 0;
const native_1 = require("@react-navigation/native");
const react_1 = __importDefault(require("react"));
const ExpoHeadModule_1 = require("./ExpoHeadModule");
const url_1 = require("./url");
const hooks_1 = require("../hooks");
function urlToId(url) {
return url.replace(/[^a-zA-Z0-9]/g, '-');
}
function getLastSegment(path) {
// Remove the extension
const lastSegment = path.split('/').pop() ?? '';
return lastSegment.replace(/\.[^/.]+$/, '').split('?')[0];
}
// TODO: Use Head Provider to collect all props so only one Head is rendered for a given route.
function useAddressableLink() {
const pathname = (0, hooks_1.useUnstableGlobalHref)();
const params = (0, hooks_1.useLocalSearchParams)();
const url = (0, url_1.getStaticUrlFromExpoRouter)(pathname);
return { url, pathname, params };
}
function useMetaChildren(children) {
return react_1.default.useMemo(() => {
const renderableChildren = [];
const metaChildren = [];
react_1.default.Children.forEach(children, (child) => {
if (!react_1.default.isValidElement(child)) {
return;
}
if (typeof child.type === 'string') {
metaChildren.push(child);
}
else {
renderableChildren.push(child);
}
});
return { children: renderableChildren, metaChildren };
}, [children]);
}
function serializedMetaChildren(meta) {
const validMeta = meta.filter((child) => child.type === 'meta' || child.type === 'title');
return validMeta.map((child) => {
if (child.type === 'title') {
return {
type: 'title',
props: {
children: typeof child.props.children === 'string' ? child.props.children : undefined,
},
};
}
return {
type: 'meta',
props: {
property: typeof child.props.property === 'string' ? child.props.property : undefined,
content: typeof child.props.content === 'string' ? child.props.content : undefined,
},
};
});
}
function useActivityFromMetaChildren(meta) {
const { url: href, pathname } = useAddressableLink();
const previousMeta = react_1.default.useRef([]);
const cachedActivity = react_1.default.useRef({});
const sortedMeta = react_1.default.useMemo(() => serializedMetaChildren(meta), [meta]);
const url = react_1.default.useMemo(() => {
const urlMeta = sortedMeta.find((child) => child.type === 'meta' && child.props.property === 'og:url');
if (urlMeta) {
// Support =`/foo/bar` -> `https://example.com/foo/bar`
if (urlMeta.props.content?.startsWith('/')) {
return (0, url_1.getStaticUrlFromExpoRouter)(urlMeta.props.content);
}
return urlMeta.props.content;
}
return href;
}, [sortedMeta, href]);
const title = react_1.default.useMemo(() => {
const titleTag = sortedMeta.find((child) => child.type === 'title');
if (titleTag) {
return titleTag.props.children ?? '';
}
const titleMeta = sortedMeta.find((child) => child.type === 'meta' && child.props.property === 'og:title');
if (titleMeta) {
return titleMeta.props.content ?? '';
}
return getLastSegment(pathname);
}, [sortedMeta, pathname]);
const activity = react_1.default.useMemo(() => {
if (!!previousMeta.current &&
!!cachedActivity.current &&
deepObjectCompare(previousMeta.current, sortedMeta)) {
return cachedActivity.current;
}
previousMeta.current = sortedMeta;
const userActivity = {};
sortedMeta.forEach((child) => {
if (
// <meta />
child.type === 'meta') {
const { property, content } = child.props;
switch (property) {
case 'og:description':
userActivity.description = content;
break;
// Custom properties
case 'expo:handoff':
userActivity.isEligibleForHandoff = isTruthy(content);
break;
case 'expo:spotlight':
userActivity.isEligibleForSearch = isTruthy(content);
break;
}
// // <meta name="keywords" content="foo,bar,baz" />
// if (["keywords"].includes(name)) {
// userActivity.keywords = Array.isArray(content)
// ? content
// : content.split(",");
// }
}
});
cachedActivity.current = userActivity;
return userActivity;
}, [meta, pathname, href]);
const parsedActivity = {
keywords: [title],
...activity,
title,
webpageURL: url,
activityType: ExpoHeadModule_1.ExpoHead.activities.INDEXED_ROUTE,
userInfo: {
// TODO: This may need to be versioned in the future, e.g. `_v1` if we change the format.
href,
},
};
return parsedActivity;
}
function isTruthy(value) {
return [true, 'true'].includes(value);
}
function HeadNative(props) {
const isFocused = (0, native_1.useIsFocused)();
if (!isFocused) {
return <UnfocusedHead />;
}
return <FocusedHead {...props}/>;
}
function UnfocusedHead(props) {
const { children } = useMetaChildren(props.children);
return <>{children}</>;
}
function FocusedHead(props) {
const { metaChildren, children } = useMetaChildren(props.children);
const activity = useActivityFromMetaChildren(metaChildren);
useRegisterCurrentActivity(activity);
return <>{children}</>;
}
// segments => activity
const activities = new Map();
function useRegisterCurrentActivity(activity) {
// ID is tied to Expo Router and agnostic of URLs to ensure dynamic parameters are not considered.
// Using all segments ensures that cascading routes are considered.
const activityId = urlToId((0, hooks_1.usePathname)() || '/');
const cascadingId = urlToId((0, hooks_1.useSegments)().join('-') || '-');
const activityIds = Array.from(activities.keys());
const cascadingActivity = react_1.default.useMemo(() => {
// Get all nested activities together, then update the id to match the current pathname.
// This enables cases like `/user/[name]/post/[id]` to match all nesting, while still having a URL-specific ID, i.e. `/user/evanbacon/post/123`
const cascadingActivity = activities.has(cascadingId)
? {
...activities.get(cascadingId),
...activity,
id: activityId,
}
: {
...activity,
id: activityId,
};
activities.set(cascadingId, cascadingActivity);
return cascadingActivity;
}, [cascadingId, activityId, activity, activityIds]);
const previousActivity = react_1.default.useRef(null);
react_1.default.useEffect(() => {
if (!cascadingActivity) {
return () => { };
}
if (!!previousActivity.current &&
deepObjectCompare(previousActivity.current, cascadingActivity)) {
return () => { };
}
previousActivity.current = cascadingActivity;
if (!cascadingActivity.id) {
throw new Error('Activity must have an ID');
}
// If no features are enabled, then skip registering the activity
if (cascadingActivity.isEligibleForHandoff || cascadingActivity.isEligibleForSearch) {
ExpoHeadModule_1.ExpoHead?.createActivity(cascadingActivity);
}
return () => { };
}, [cascadingActivity]);
react_1.default.useEffect(() => {
return () => {
if (activityId) {
ExpoHeadModule_1.ExpoHead?.suspendActivity(activityId);
}
};
}, [activityId]);
}
function deepObjectCompare(a, b) {
if (typeof a !== typeof b) {
return false;
}
if (typeof a === 'object') {
if (Array.isArray(a) !== Array.isArray(b)) {
return false;
}
if (Array.isArray(a)) {
if (a.length !== b.length) {
return false;
}
return a.every((item, index) => deepObjectCompare(item, b[index]));
}
// handle null
if (a === null || b === null) {
return a === b;
}
const aKeys = Object.keys(a);
const bKeys = Object.keys(b);
if (aKeys.length !== bKeys.length) {
return false;
}
return aKeys.every((key) => deepObjectCompare(a[key], b[key]));
}
return a === b;
}
HeadNative.Provider = react_1.default.Fragment;
function HeadShim(props) {
return null;
}
HeadShim.Provider = react_1.default.Fragment;
// Native Head is only enabled in bare iOS apps.
exports.Head = ExpoHeadModule_1.ExpoHead ? HeadNative : HeadShim;
//# sourceMappingURL=ExpoHead.ios.js.map
;