expo-router 1.2.0 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/_root.tsx CHANGED
@@ -1,13 +1,14 @@
1
+ /// <reference path="metro-require.d.ts" />
2
+
1
3
  import "@expo/metro-runtime";
2
- import React from "react";
3
4
 
4
5
  import { ExpoRoot } from "expo-router";
6
+ import React from "react";
5
7
 
6
8
  import { getNavigationConfig } from "./src/getLinkingConfig";
7
9
  import { getRoutes } from "./src/getRoutes";
8
10
 
9
- // @ts-expect-error: Not sure
10
- const ctx = require.context(process.env.EXPO_ROUTER_APP_ROOT);
11
+ const ctx = require.context(process.env.EXPO_ROUTER_APP_ROOT!);
11
12
 
12
13
  // Must be exported or Fast Refresh won't update the context >:[
13
14
  export default function ExpoRouterRoot() {
@@ -0,0 +1,52 @@
1
+ // Based on https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/webpack-env/index.d.ts
2
+ // Adds support for the runtime `require.context` method.
3
+ // https://github.com/facebook/metro/pull/822/
4
+
5
+ declare var module: NodeModule;
6
+
7
+ declare namespace __MetroModuleApi {
8
+ interface RequireContext {
9
+ /** Return the keys that can be resolved. */
10
+ keys(): string[];
11
+ (id: string): any;
12
+ <T>(id: string): T;
13
+ /** **Unimplemented:** Return the module identifier for a user request. */
14
+ resolve(id: string): string;
15
+ /** **Unimplemented:** Readable identifier for the context module. */
16
+ id: string;
17
+ }
18
+
19
+ interface RequireFunction {
20
+ /**
21
+ * Returns the exports from a dependency. The call is sync. No request to the server is fired. The compiler ensures that the dependency is available.
22
+ */
23
+ (path: string): any;
24
+ <T>(path: string): T;
25
+
26
+ /**
27
+ * **Experimental:** Import all modules in a given directory. This module dynamically updates when the files in a directory are added or removed.
28
+ *
29
+ * **Enabling:** This feature can be enabled by setting the `transformer.unstable_allowRequireContext` property to `true` in your Metro configuration.
30
+ *
31
+ * @param path File path pointing to the directory to require.
32
+ * @param recursive Should search for files recursively. Optional, default `true` when `require.context` is used.
33
+ * @param filter Filename filter pattern for use in `require.context`. Optional, default `.*` (any file) when `require.context` is used.
34
+ * @param mode Mode for resolving dynamic dependencies. Defaults to `sync`.
35
+ */
36
+ context(
37
+ path: string,
38
+ recursive?: boolean,
39
+ filter?: RegExp,
40
+ mode?: "sync" | "eager" | "weak" | "lazy" | "lazy-once"
41
+ ): RequireContext;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Declare process variable
47
+ */
48
+ declare namespace NodeJS {
49
+ interface Require extends __MetroModuleApi.RequireFunction {}
50
+ }
51
+
52
+ declare var require: NodeRequire;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-router",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "main": "src/index.tsx",
5
5
  "types": "src/index.tsx",
6
6
  "files": [
@@ -13,7 +13,8 @@
13
13
  "drawer.ts",
14
14
  "stack.ts",
15
15
  "tabs.ts",
16
- "head.ts"
16
+ "head.ts",
17
+ "metro-require.d.ts"
17
18
  ],
18
19
  "repository": {
19
20
  "url": "https://github.com/expo/router.git",
@@ -62,6 +63,7 @@
62
63
  },
63
64
  "devDependencies": {
64
65
  "@react-navigation/drawer": "^6.6.2",
66
+ "@types/url-parse": "^1.4.8",
65
67
  "expo-splash-screen": "~0.18.1",
66
68
  "expo-status-bar": "~1.4.4",
67
69
  "react-native-gesture-handler": "~2.9.0",
@@ -71,12 +73,13 @@
71
73
  },
72
74
  "dependencies": {
73
75
  "@bacons/react-views": "^1.1.3",
74
- "@expo/metro-runtime": "1.0.0",
76
+ "@expo/metro-runtime": "1.1.0",
75
77
  "@radix-ui/react-slot": "^1.0.0",
76
78
  "@react-navigation/bottom-tabs": "~6.5.7",
77
79
  "@react-navigation/native": "~6.1.6",
78
80
  "@react-navigation/native-stack": "~6.9.12",
79
81
  "expo-splash-screen": "*",
82
+ "query-string": "7.1.3",
80
83
  "react-helmet-async": "^1.3.0",
81
84
  "url": "^0.11.0"
82
85
  }
@@ -156,7 +156,7 @@ export function getNormalizedStatePath({
156
156
  prev[key] = decodeURIComponent(value as string);
157
157
  }
158
158
  return prev;
159
- }, {}),
159
+ }, {} as SearchParams),
160
160
  };
161
161
  }
162
162
 
@@ -7,7 +7,7 @@ import { RootNavigationRef } from "./useRootNavigation";
7
7
  import { useRootRouteNodeContext } from "./useRootRouteNodeContext";
8
8
  import { SplashScreen } from "./views/Splash";
9
9
 
10
- const navigationRef = createNavigationContainerRef();
10
+ const navigationRef = createNavigationContainerRef<Record<string, unknown>>();
11
11
 
12
12
  /** Get the root navigation container ref. */
13
13
  export function getNavigationContainerRef() {
@@ -39,6 +39,8 @@ type CustomRoute = Route<string> & {
39
39
  state?: State;
40
40
  };
41
41
 
42
+ const DEFAULT_SCREENS: PathConfigMap<object> = {};
43
+
42
44
  const getActiveRoute = (state: State): { name: string; params?: object } => {
43
45
  const route =
44
46
  typeof state.index === "number"
@@ -118,22 +120,20 @@ function encodeURIComponentPreservingBrackets(str: string) {
118
120
  */
119
121
  export default function getPathFromState<ParamList extends object>(
120
122
  state: State,
121
- // @ts-expect-error: non-standard options
122
123
  _options?: Options<ParamList> & {
123
124
  preserveGroups?: boolean;
124
125
  preserveDynamicRoutes?: boolean;
125
- } = {}
126
+ }
126
127
  ): string {
127
128
  return getPathDataFromState(state, _options).path;
128
129
  }
129
130
 
130
131
  export function getPathDataFromState<ParamList extends object>(
131
132
  state: State,
132
- // @ts-expect-error: non-standard options
133
- _options?: Options<ParamList> & {
133
+ _options: Options<ParamList> & {
134
134
  preserveGroups?: boolean;
135
135
  preserveDynamicRoutes?: boolean;
136
- } = {}
136
+ } = { screens: DEFAULT_SCREENS }
137
137
  ) {
138
138
  if (state == null) {
139
139
  throw Error(
@@ -143,13 +143,10 @@ export function getPathDataFromState<ParamList extends object>(
143
143
 
144
144
  const { preserveGroups, preserveDynamicRoutes, ...options } = _options;
145
145
 
146
- if (_options) {
147
- validatePathConfig(options);
148
- }
146
+ validatePathConfig(options);
149
147
 
150
- const screens = options?.screens;
151
148
  // Expo Router disallows usage without a linking config.
152
- if (!screens) {
149
+ if (Object.is(options.screens, DEFAULT_SCREENS)) {
153
150
  throw Error(
154
151
  "You must pass a 'screens' object to 'getPathFromState' to generate a path."
155
152
  );
@@ -158,7 +155,7 @@ export function getPathDataFromState<ParamList extends object>(
158
155
  return getPathFromResolvedState(
159
156
  state,
160
157
  // Create a normalized configs object which will be easier to use
161
- createNormalizedConfigs(screens),
158
+ createNormalizedConfigs(options.screens),
162
159
  { preserveGroups, preserveDynamicRoutes }
163
160
  );
164
161
  }
@@ -515,7 +512,7 @@ function getParamsWithConventionsCollapsed({
515
512
  routeName: string;
516
513
  params: object;
517
514
  }): Record<string, string> {
518
- const processedParams = { ...params };
515
+ const processedParams: Record<string, string> = { ...params };
519
516
 
520
517
  // Remove the params present in the pattern since we'll only use the rest for query string
521
518
 
@@ -472,6 +472,7 @@ const createNormalizedConfigs = (
472
472
 
473
473
  parentScreens.push(screen);
474
474
 
475
+ // @ts-expect-error
475
476
  const config = routeConfig[screen];
476
477
 
477
478
  if (typeof config === "string") {
@@ -1,3 +1,4 @@
1
+ // @ts-expect-error
1
2
  import useLinking from "@react-navigation/native/lib/module/useLinking";
2
3
 
3
4
  export default useLinking as typeof import("./useLinking.native").default;
package/src/link/path.ts CHANGED
@@ -21,7 +21,7 @@
21
21
 
22
22
  // https://github.com/browserify/path-browserify/blob/master/index.js
23
23
 
24
- function assertPath(path) {
24
+ function assertPath(path: string) {
25
25
  if (typeof path !== "string") {
26
26
  throw new TypeError(
27
27
  "Path must be a string. Received " + JSON.stringify(path)
@@ -30,7 +30,7 @@ function assertPath(path) {
30
30
  }
31
31
 
32
32
  // Resolves . and .. elements in a path with directory names
33
- function normalizeStringPosix(path, allowAboveRoot) {
33
+ function normalizeStringPosix(path: string, allowAboveRoot?: boolean) {
34
34
  let res = "";
35
35
  let lastSegmentLength = 0;
36
36
  let lastSlash = -1;
@@ -98,7 +98,7 @@ function normalizeStringPosix(path, allowAboveRoot) {
98
98
  }
99
99
 
100
100
  // path.resolve([from ...], to)
101
- export function resolve(...segments) {
101
+ export function resolve(...segments: string[]) {
102
102
  let resolvedPath = "";
103
103
  let resolvedAbsolute = false;
104
104
 
@@ -1,4 +1,4 @@
1
- import { InitialState } from "@react-navigation/native";
1
+ import { InitialState, NavigationState } from "@react-navigation/native";
2
2
 
3
3
  import { ResultState } from "../fork/getStateFromPath";
4
4
 
@@ -36,7 +36,7 @@ export function isMovingToSiblingRoute(
36
36
  let currentRoot: InitialState | undefined = rootState;
37
37
 
38
38
  while (current?.routes?.[current?.routes?.length - 1].state != null) {
39
- const nextRoute = current?.routes?.[current?.routes?.length - 1];
39
+ const nextRoute: any = current?.routes?.[current?.routes?.length - 1];
40
40
 
41
41
  if (
42
42
  // Has more
@@ -74,9 +74,9 @@ export function getQualifiedStateForTopOfTargetState(
74
74
  let currentRoot: InitialState | undefined = rootState;
75
75
 
76
76
  while (current?.routes?.[current?.routes?.length - 1].state != null) {
77
- const nextRoute = current?.routes?.[current?.routes?.length - 1];
77
+ const nextRoute: any = current?.routes?.[current?.routes?.length - 1];
78
78
 
79
- const nextCurrentRoot = currentRoot?.routes?.find(
79
+ const nextCurrentRoot: InitialState | undefined = currentRoot?.routes?.find(
80
80
  (route) => route.name === nextRoute.name
81
81
  )?.state;
82
82
 
@@ -95,7 +95,8 @@ export function getQualifiedStateForTopOfTargetState(
95
95
  return currentRoot;
96
96
  }
97
97
 
98
- type SubState = {
98
+ type SubState = NavigationState & {
99
+ key?: string;
99
100
  type: string;
100
101
  routes?: { name: string; state?: SubState }[];
101
102
  index?: number;
@@ -54,7 +54,6 @@ export function useLinkToPath() {
54
54
  if (href.startsWith(".")) {
55
55
  let base = linking.getPathFromState?.(navigation.getRootState(), {
56
56
  ...linking.config,
57
- // @ts-expect-error: non-standard option
58
57
  preserveGroups: true,
59
58
  });
60
59
 
@@ -123,8 +122,8 @@ export function useLinkToPath() {
123
122
  isAbsoluteInitialRoute(action)
124
123
  ) {
125
124
  const earliest = getEarliestMismatchedRoute(
126
- // @ts-expect-error
127
125
  rootState,
126
+ // @ts-expect-error
128
127
  action.payload
129
128
  );
130
129
  if (earliest) {
@@ -5,20 +5,26 @@ import { useLinkToPath } from "./useLinkToPath";
5
5
  import { stripGroupSegmentsFromPath } from "../matchers";
6
6
 
7
7
  function eventShouldPreventDefault(
8
- e?: React.MouseEvent<HTMLAnchorElement, MouseEvent> | GestureResponderEvent
8
+ e: React.MouseEvent<HTMLAnchorElement, MouseEvent> | GestureResponderEvent
9
9
  ): boolean {
10
+ if (e?.defaultPrevented) {
11
+ return false;
12
+ }
13
+
10
14
  if (
11
- !!e &&
12
- !e.defaultPrevented && // onPress prevented default
13
- // @ts-expect-error: these properties exist on web, but not in React Native
14
- !(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) && // ignore clicks with modifier keys
15
- // @ts-expect-error: these properties exist on web, but not in React Native
16
- (e.button == null || e.button === 0) && // ignore everything but left clicks
17
- // @ts-expect-error: these properties exist on web, but not in React Native
18
- [undefined, null, "", "self"].includes(e.currentTarget?.target) // let browser handle "target=_blank" etc.
15
+ // Only check MouseEvents
16
+ "button" in e &&
17
+ // ignore clicks with modifier keys
18
+ !e.metaKey &&
19
+ !e.altKey &&
20
+ !e.ctrlKey &&
21
+ !e.shiftKey &&
22
+ (e.button == null || e.button === 0) && // Only accept left clicks
23
+ [undefined, null, "", "self"].includes(e.currentTarget.target) // let browser handle "target=_blank" etc.
19
24
  ) {
20
25
  return true;
21
26
  }
27
+
22
28
  return false;
23
29
  }
24
30
 
@@ -5,20 +5,31 @@ import {
5
5
  } from "@react-navigation/native";
6
6
  import * as React from "react";
7
7
 
8
- export function useLinkingContext(): Required<
8
+ import getPathFromState from "../fork/getPathFromState";
9
+
10
+ export type RouterLinkingContext = Required<
9
11
  Omit<LinkingOptions<ParamListBase>, "filter" | "enabled">
10
- > {
12
+ > & {
13
+ getPathFromState: typeof getPathFromState;
14
+ };
15
+
16
+ export function useLinkingContext(): RouterLinkingContext {
11
17
  const linking = React.useContext(LinkingContext);
12
18
 
13
19
  const { options } = linking;
14
20
 
21
+ assertLinkingOptions(options);
22
+
23
+ return options;
24
+ }
25
+
26
+ function assertLinkingOptions(
27
+ options: LinkingOptions<ParamListBase> | undefined
28
+ ): asserts options is RouterLinkingContext {
15
29
  if (!options?.config) {
16
30
  // This should never happen in Expo Router.
17
31
  throw new Error(
18
32
  "Couldn't find a linking config. Is your component inside a navigator?"
19
33
  );
20
34
  }
21
-
22
- // @ts-expect-error: non-standard option
23
- return options;
24
35
  }
@@ -47,11 +47,8 @@ export function useRouter(): Router {
47
47
  push,
48
48
  back,
49
49
  replace,
50
- setParams: (params) => {
51
- root?.current?.setParams(
52
- // @ts-expect-error
53
- params
54
- );
50
+ setParams: (params = {}) => {
51
+ root?.current?.setParams(params);
55
52
  },
56
53
  // TODO(EvanBacon): add `reload`
57
54
  // TODO(EvanBacon): add `canGoBack` but maybe more like a `hasContext`
@@ -4,7 +4,7 @@ import { Platform, View } from "react-native";
4
4
  import registerRootComponent from "./fork/expo/registerRootComponent";
5
5
  import { SplashScreen } from "./views/Splash";
6
6
 
7
- function isBaseObject(obj) {
7
+ function isBaseObject(obj: any) {
8
8
  if (Object.prototype.toString.call(obj) !== "[object Object]") {
9
9
  return false;
10
10
  }
@@ -15,7 +15,7 @@ function isBaseObject(obj) {
15
15
  return proto === Object.prototype;
16
16
  }
17
17
 
18
- function isErrorShaped(error) {
18
+ function isErrorShaped(error: any): error is Error {
19
19
  return (
20
20
  error &&
21
21
  typeof error === "object" &&
@@ -28,7 +28,7 @@ function isErrorShaped(error) {
28
28
  * After we throw this error, any number of tools could handle it.
29
29
  * This check ensures the error is always in a reason state before surfacing it to the runtime.
30
30
  */
31
- function convertError(error) {
31
+ function convertError(error: any) {
32
32
  if (isErrorShaped(error)) {
33
33
  return error;
34
34
  }
@@ -30,12 +30,15 @@ export function getStaticContent(location: URL): string {
30
30
  // TODO: Use RNW view after they fix hydration for React 18
31
31
  // https://github.com/necolas/react-native-web/blob/e8098fd029102d7801c32c1ede792bce01808c00/packages/react-native-web/src/exports/render/index.js#L10
32
32
  // Otherwise this wraps the app with two extra divs
33
- children: (
34
- // Inject the root tag
35
- <div id="root">
33
+ children:
34
+ // Inject the root tag using createElement to prevent any transforms like the ones in `@expo/html-elements`.
35
+ React.createElement(
36
+ "div",
37
+ {
38
+ id: "root",
39
+ },
36
40
  <App />
37
- </div>
38
- ),
41
+ ),
39
42
  });
40
43
 
41
44
  const html = ReactDOMServer.renderToString(
@@ -113,7 +116,7 @@ function StyleReset() {
113
116
  }
114
117
 
115
118
  // TODO(EvanBacon): Expose this to the developer
116
- export function Root({ children }) {
119
+ export function Root({ children }: { children: React.ReactNode }) {
117
120
  return (
118
121
  <html lang="en" style={{ height: "100%" }}>
119
122
  <head>
@@ -1,4 +1,5 @@
1
1
  // TODO: This is fragile and only works on web
2
+ // @ts-expect-error
2
3
  import ServerContext from "@react-navigation/native/lib/module/ServerContext";
3
4
  import React from "react";
4
5
 
@@ -165,7 +165,7 @@ export function createGetIdForRoute(
165
165
  if (!route.dynamic?.length) {
166
166
  return undefined;
167
167
  }
168
- return ({ params }) => {
168
+ return ({ params }: { params?: Record<string, any> }) => {
169
169
  const getPreferredId = (segment: DynamicConvention) => {
170
170
  // Params can be undefined when there are no params in the route.
171
171
  const preferredId = params?.[segment.name];
@@ -10,12 +10,14 @@ import { useContextKey } from "../Route";
10
10
  import { useFilterScreenChildren } from "../layouts/withLayoutContext";
11
11
  import { useSortedScreens } from "../useScreens";
12
12
 
13
+ type NavigatorTypes = ReturnType<typeof useNavigationBuilder>;
14
+
13
15
  // TODO: This might already exist upstream, maybe something like `useCurrentRender` ?
14
16
  export const NavigatorContext = React.createContext<{
15
17
  contextKey: string;
16
- state: any;
17
- navigation: any;
18
- descriptors: any;
18
+ state: NavigatorTypes["state"];
19
+ navigation: NavigatorTypes["navigation"];
20
+ descriptors: NavigatorTypes["descriptors"];
19
21
  router: RouterFactory<any, any, any>;
20
22
  } | null>(null);
21
23