expo-router 2.0.8 → 2.0.10

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.
@@ -1 +1 @@
1
- {"version":3,"file":"getRoutes.d.ts","sourceRoot":"","sources":["../src/getRoutes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAW5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C,MAAM,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,EAAE,YAAY,GAAG,WAAW,CAAC,GAAG;IACnE,yBAAyB;IACzB,cAAc,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,KAAK,QAAQ,GAAG;IACd,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,QAAQ,EAAE,CAAC;IACrB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,8CAA8C;IAC9C,IAAI,EAAE,QAAQ,GAAG,IAAI,CAAC;CACvB,CAAC;AAEF,KAAK,OAAO,GAAG;IACb,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB,CAAC;AAEF,oEAAoE;AACpE,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,QAAQ,CA+C5D;AAyBD,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,MAAM,GACX,iBAAiB,GAAG,IAAI,CAK1B;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC,SAAS,CAAC,CAMlE;AAsND;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,QAiBxD;AAED,sEAAsE;AACtE,wBAAgB,SAAS,CACvB,aAAa,EAAE,cAAc,EAC7B,OAAO,CAAC,EAAE,OAAO,GAChB,SAAS,GAAG,IAAI,CAclB;AAED,wBAAsB,cAAc,CAClC,aAAa,EAAE,cAAc,EAC7B,OAAO,CAAC,EAAE,OAAO,GAChB,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAY3B;AAUD,+CAA+C;AAC/C,wBAAgB,cAAc,CAC5B,aAAa,EAAE,cAAc,EAC7B,OAAO,CAAC,EAAE,OAAO,GAChB,SAAS,GAAG,IAAI,CAIlB;AAYD,wBAAsB,mBAAmB,CACvC,aAAa,EAAE,cAAc,EAC7B,OAAO,CAAC,EAAE,OAAO,GAChB,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAI3B;AA4CD;;;GAGG;AACH,wBAAgB,8BAA8B,CAC5C,MAAM,EAAE,SAAS,GAChB,SAAS,GAAG,IAAI,CAkBlB"}
1
+ {"version":3,"file":"getRoutes.d.ts","sourceRoot":"","sources":["../src/getRoutes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAW5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C,MAAM,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,EAAE,YAAY,GAAG,WAAW,CAAC,GAAG;IACnE,yBAAyB;IACzB,cAAc,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,KAAK,QAAQ,GAAG;IACd,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,QAAQ,EAAE,CAAC;IACrB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,8CAA8C;IAC9C,IAAI,EAAE,QAAQ,GAAG,IAAI,CAAC;CACvB,CAAC;AAEF,KAAK,OAAO,GAAG;IACb,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB,CAAC;AAEF,oEAAoE;AACpE,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,QAAQ,CA+C5D;AAyBD,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,MAAM,GACX,iBAAiB,GAAG,IAAI,CAK1B;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC,SAAS,CAAC,CAMlE;AA+ND;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,QAiBxD;AAED,sEAAsE;AACtE,wBAAgB,SAAS,CACvB,aAAa,EAAE,cAAc,EAC7B,OAAO,CAAC,EAAE,OAAO,GAChB,SAAS,GAAG,IAAI,CAclB;AAED,wBAAsB,cAAc,CAClC,aAAa,EAAE,cAAc,EAC7B,OAAO,CAAC,EAAE,OAAO,GAChB,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAY3B;AAUD,+CAA+C;AAC/C,wBAAgB,cAAc,CAC5B,aAAa,EAAE,cAAc,EAC7B,OAAO,CAAC,EAAE,OAAO,GAChB,SAAS,GAAG,IAAI,CAIlB;AAYD,wBAAsB,mBAAmB,CACvC,aAAa,EAAE,cAAc,EAC7B,OAAO,CAAC,EAAE,OAAO,GAChB,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAI3B;AA4CD;;;GAGG;AACH,wBAAgB,8BAA8B,CAC5C,MAAM,EAAE,SAAS,GAChB,SAAS,GAAG,IAAI,CAkBlB"}
@@ -1,3 +1,9 @@
1
1
  export declare function useWarnOnce(message: string, guard?: unknown, key?: string): void;
2
- export declare function useDeprecated(message: string, guard?: unknown, key?: string): void;
2
+ export declare function useDeprecated(
3
+ /** The deprecated message to display */
4
+ message: string,
5
+ /** The guard to cause the warning to being displayed */
6
+ guard?: unknown,
7
+ /** The key to use for the warning. Used to detect if the warning has already been shown. */
8
+ key?: string): void;
3
9
  //# sourceMappingURL=useDeprecated.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useDeprecated.d.ts","sourceRoot":"","sources":["../src/useDeprecated.ts"],"names":[],"mappings":"AAaA,wBAAgB,WAAW,CACzB,OAAO,EAAE,MAAM,EACf,KAAK,GAAE,OAAc,EACrB,GAAG,SAAU,QAUd;AAED,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,KAAK,GAAE,OAAc,EACrB,GAAG,SAAU,QAGd"}
1
+ {"version":3,"file":"useDeprecated.d.ts","sourceRoot":"","sources":["../src/useDeprecated.ts"],"names":[],"mappings":"AAaA,wBAAgB,WAAW,CACzB,OAAO,EAAE,MAAM,EACf,KAAK,GAAE,OAAc,EACrB,GAAG,SAAU,QAUd;AAED,wBAAgB,aAAa;AAC3B,wCAAwC;AACxC,OAAO,EAAE,MAAM;AACf,wDAAwD;AACxD,KAAK,GAAE,OAAc;AACrB,4FAA4F;AAC5F,GAAG,SAAU,QAGd"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-router",
3
- "version": "2.0.8",
3
+ "version": "2.0.10",
4
4
  "main": "src/index.tsx",
5
5
  "types": "build/index.d.ts",
6
6
  "files": [
@@ -105,12 +105,12 @@
105
105
  },
106
106
  "dependencies": {
107
107
  "@bacons/react-views": "^1.1.3",
108
- "@expo/metro-runtime": "2.2.10",
108
+ "@expo/metro-runtime": "2.2.12",
109
109
  "@radix-ui/react-slot": "1.0.1",
110
110
  "@react-navigation/bottom-tabs": "~6.5.7",
111
111
  "@react-navigation/native": "~6.1.6",
112
112
  "@react-navigation/native-stack": "~6.9.12",
113
- "expo-head": "0.0.14",
113
+ "expo-head": "0.0.16",
114
114
  "expo-splash-screen": "~0.20.2",
115
115
  "query-string": "7.1.3",
116
116
  "react-helmet-async": "^1.3.0",
package/src/getRoutes.ts CHANGED
@@ -169,21 +169,6 @@ function applyDefaultInitialRouteName(node: RouteNode): RouteNode {
169
169
  };
170
170
  }
171
171
 
172
- function cloneGroupRoute(
173
- node: RouteNode,
174
- { name: nextName }: { name: string }
175
- ): RouteNode {
176
- const groupName = `(${nextName})`;
177
- const parts = node.contextKey.split("/");
178
- parts[parts.length - 2] = groupName;
179
-
180
- return {
181
- ...node,
182
- route: groupName,
183
- contextKey: parts.join("/"),
184
- };
185
- }
186
-
187
172
  function folderNodeToRouteNode({
188
173
  name,
189
174
  children,
@@ -212,25 +197,8 @@ function fileNodeToRouteNode(tree: TreeNode): RouteNode[] | null {
212
197
 
213
198
  const dynamic = generateDynamic(name);
214
199
 
215
- const groupName = matchGroupName(name);
216
- const multiGroup = groupName?.includes(",");
217
-
218
- const clones = multiGroup
219
- ? groupName!.split(",").map((v) => ({ name: v.trim() }))
220
- : null;
221
-
222
- // Assert duplicates:
223
- if (clones) {
224
- const names = new Set<string>();
225
- for (const clone of clones) {
226
- if (names.has(clone.name)) {
227
- throw new Error(
228
- `Array syntax cannot contain duplicate group name "${clone.name}" in "${node.contextKey}".`
229
- );
230
- }
231
- names.add(clone.name);
232
- }
233
- }
200
+ const clones = extrapolateGroupRoutes(name, node.contextKey);
201
+ clones.delete(name);
234
202
 
235
203
  const output = {
236
204
  loadRoute: node.loadRoute,
@@ -240,9 +208,13 @@ function fileNodeToRouteNode(tree: TreeNode): RouteNode[] | null {
240
208
  dynamic,
241
209
  };
242
210
 
243
- if (Array.isArray(clones)) {
244
- return clones.map((clone) =>
245
- applyDefaultInitialRouteName(cloneGroupRoute({ ...output }, clone))
211
+ if (clones.size) {
212
+ return [...clones].map((clone) =>
213
+ applyDefaultInitialRouteName({
214
+ ...output,
215
+ contextKey: node.contextKey.replace(output.route, clone),
216
+ route: clone,
217
+ })
246
218
  );
247
219
  }
248
220
 
@@ -257,6 +229,43 @@ function fileNodeToRouteNode(tree: TreeNode): RouteNode[] | null {
257
229
  ];
258
230
  }
259
231
 
232
+ function extrapolateGroupRoutes(
233
+ route: string,
234
+ contextKey: string,
235
+ routes: Set<string> = new Set()
236
+ ): Set<string> {
237
+ const match = matchGroupName(route);
238
+
239
+ if (!match) {
240
+ routes.add(route);
241
+ return routes;
242
+ }
243
+
244
+ const groups = match?.split(",");
245
+ const groupsSet = new Set(groups);
246
+
247
+ if (groupsSet.size !== groups.length) {
248
+ throw new Error(
249
+ `Array syntax cannot contain duplicate group name "${groups}" in "${contextKey}".`
250
+ );
251
+ }
252
+
253
+ if (groups.length === 1) {
254
+ routes.add(route);
255
+ return routes;
256
+ }
257
+
258
+ for (const group of groups) {
259
+ extrapolateGroupRoutes(
260
+ route.replace(match, group.trim()),
261
+ contextKey,
262
+ routes
263
+ );
264
+ }
265
+
266
+ return routes;
267
+ }
268
+
260
269
  function treeNodeToRouteNode(tree: TreeNode): RouteNode[] | null {
261
270
  if (tree.node) {
262
271
  return fileNodeToRouteNode(tree);
@@ -75,9 +75,11 @@ export function linkTo(this: RouterStore, href: string, event?: string) {
75
75
  return;
76
76
  }
77
77
 
78
+ const rootState = navigationRef.getRootState();
79
+
78
80
  if (href.startsWith(".")) {
79
81
  let base =
80
- this.linking.getPathFromState?.(navigationRef.getRootState(), {
82
+ this.linking.getPathFromState?.(rootState, {
81
83
  screens: [],
82
84
  preserveGroups: true,
83
85
  }) ?? "";
@@ -99,11 +101,9 @@ export function linkTo(this: RouterStore, href: string, event?: string) {
99
101
 
100
102
  switch (event) {
101
103
  case "REPLACE":
102
- return navigationRef.dispatch(
103
- getNavigateReplaceAction(state, navigationRef.getRootState())
104
- );
104
+ return navigationRef.dispatch(getNavigateReplaceAction(state, rootState));
105
105
  default:
106
- return navigationRef.dispatch(getNavigatePushAction(state));
106
+ return navigationRef.dispatch(getNavigatePushAction(state, rootState));
107
107
  }
108
108
  }
109
109
 
@@ -132,10 +132,11 @@ function rewriteNavigationStateToParams(
132
132
  return JSON.parse(JSON.stringify(params));
133
133
  }
134
134
 
135
- function getNavigatePushAction(state: ResultState) {
135
+ function getNavigatePushAction(state: ResultState, rootState: NavigationState) {
136
136
  const { screen, params } = rewriteNavigationStateToParams(state);
137
137
  return {
138
138
  type: "NAVIGATE",
139
+ target: rootState.key,
139
140
  payload: {
140
141
  name: screen,
141
142
  params,
@@ -144,12 +145,12 @@ function getNavigatePushAction(state: ResultState) {
144
145
  }
145
146
 
146
147
  function getNavigateReplaceAction(
147
- previousState: ResultState,
148
+ state: ResultState,
148
149
  parentState: NavigationState,
149
150
  lastNavigatorSupportingReplace: NavigationState = parentState
150
151
  ): NavigationAction {
151
152
  // We should always have at least one route in the state
152
- const state = previousState.routes.at(-1)!;
153
+ const route = state.routes.at(-1)!;
153
154
 
154
155
  // Only these navigators support replace
155
156
  if (parentState.type === "stack" || parentState.type === "tab") {
@@ -157,29 +158,29 @@ function getNavigateReplaceAction(
157
158
  }
158
159
 
159
160
  const currentRoute = parentState.routes.find(
160
- (route) => route.name === state.name
161
+ (parentRoute) => parentRoute.name === route.name
161
162
  );
162
163
  const routesAreEqual = parentState.routes[parentState.index] === currentRoute;
163
164
 
164
165
  // If there is nested state and the routes are equal, we should keep going down the tree
165
- if (state.state && routesAreEqual && currentRoute.state) {
166
+ if (route.state && routesAreEqual && currentRoute.state) {
166
167
  return getNavigateReplaceAction(
167
- state.state,
168
+ route.state,
168
169
  currentRoute.state as any,
169
170
  lastNavigatorSupportingReplace
170
171
  );
171
172
  }
172
173
 
173
174
  // Either we reached the bottom of the state or the point where the routes diverged
174
- const { screen, params } = rewriteNavigationStateToParams(previousState);
175
+ const { screen, params } = rewriteNavigationStateToParams(state);
176
+
175
177
  return {
176
178
  type:
177
179
  lastNavigatorSupportingReplace.type === "stack" ? "REPLACE" : "JUMP_TO",
180
+ target: lastNavigatorSupportingReplace?.key,
178
181
  payload: {
179
182
  name: screen,
180
183
  params,
181
- // Ensure that the last navigator supporting replace is the one that handles the action
182
- source: lastNavigatorSupportingReplace?.key,
183
184
  },
184
185
  };
185
186
  }
package/src/matchers.tsx CHANGED
@@ -12,7 +12,7 @@ export function matchDeepDynamicRouteName(name: string): string | undefined {
12
12
 
13
13
  /** Match `(page)` -> `page` */
14
14
  export function matchGroupName(name: string): string | undefined {
15
- return name.match(/^\(([^/]+?)\)$/)?.[1];
15
+ return name.match(/^(?:[^\\(\\)])*?\(([^\\/]+)\).*?$/)?.[1];
16
16
  }
17
17
 
18
18
  export function getNameFromFilePath(name: string): string {
@@ -17,7 +17,7 @@ export function useWarnOnce(
17
17
  key = message
18
18
  ) {
19
19
  // useLayoutEffect typically doesn't run in node environments.
20
- // Combined with skipWarn, this should prevent unwanted warnings
20
+ // Combined with skipWarn, this should prevent unwanted warnings during SSR rendering
21
21
  useLayoutEffect(() => {
22
22
  if (guard && canWarn && !warned.has(key)) {
23
23
  warned.add(key);
@@ -27,8 +27,11 @@ export function useWarnOnce(
27
27
  }
28
28
 
29
29
  export function useDeprecated(
30
+ /** The deprecated message to display */
30
31
  message: string,
32
+ /** The guard to cause the warning to being displayed */
31
33
  guard: unknown = true,
34
+ /** The key to use for the warning. Used to detect if the warning has already been shown. */
32
35
  key = message
33
36
  ) {
34
37
  return useWarnOnce(key, guard, `Expo Router: ${message}`);
@@ -48,7 +48,7 @@ export function Screen<TOptions extends object = object>({
48
48
  // eslint-disable-next-line react-hooks/rules-of-hooks
49
49
  useDeprecated(
50
50
  "The `redirect` prop on <Screen /> is deprecated and will be removed. Please use `router.redirect` instead",
51
- redirect
51
+ Boolean(redirect)
52
52
  );
53
53
  }
54
54