expo-router 1.4.3 → 1.5.0

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/_entry.tsx CHANGED
@@ -9,7 +9,13 @@ import { getNavigationConfig } from "./src/getLinkingConfig";
9
9
  import { getRoutes } from "./src/getRoutes";
10
10
  import { loadStaticParamsAsync } from "./src/loadStaticParamsAsync";
11
11
 
12
- export const ctx = require.context(process.env.EXPO_ROUTER_APP_ROOT!);
12
+ export const ctx = require.context(
13
+ process.env.EXPO_ROUTER_APP_ROOT!,
14
+ true,
15
+ /.*/,
16
+ // @ts-expect-error
17
+ process.env.EXPO_ROUTER_IMPORT_MODE!
18
+ );
13
19
 
14
20
  // Must be exported or Fast Refresh won't update the context >:[
15
21
  export default function ExpoRouterRoot() {
Binary file
package/assets/file.png CHANGED
Binary file
Binary file
package/assets/pkg.png CHANGED
Binary file
package/babel.js CHANGED
@@ -17,6 +17,46 @@ function getExpoAppManifest(projectRoot) {
17
17
  return JSON.stringify(exp);
18
18
  }
19
19
 
20
+ let config;
21
+
22
+ function getConfigMemo(projectRoot) {
23
+ if (!config) {
24
+ config = getConfig(projectRoot);
25
+ }
26
+ return config;
27
+ }
28
+
29
+ function getExpoRouterImportMode(projectRoot, platform) {
30
+ if (process.env.EXPO_ROUTER_IMPORT_MODE) {
31
+ return process.env.EXPO_ROUTER_IMPORT_MODE;
32
+ }
33
+ const { exp } = getConfigMemo(projectRoot);
34
+ let mode = [process.env.NODE_ENV, true].includes(
35
+ exp.extra?.router?.asyncRoutes
36
+ )
37
+ ? "lazy"
38
+ : "sync";
39
+
40
+ // TODO: Production bundle splitting
41
+
42
+ if (process.env.NODE_ENV === "production" && mode === "lazy") {
43
+ throw new Error(
44
+ "Async routes are not supported in production yet. Set `extra.router.asyncRoutes` to `development`, `false`, or `undefined`."
45
+ );
46
+ }
47
+
48
+ // NOTE: This is a temporary workaround for static rendering on web.
49
+ if (platform === "web" && process.env.EXPO_USE_STATIC) {
50
+ mode = "sync";
51
+ }
52
+
53
+ // Development
54
+ debug("Router import mode", mode);
55
+
56
+ process.env.EXPO_ROUTER_IMPORT_MODE = mode;
57
+ return mode;
58
+ }
59
+
20
60
  function getExpoRouterAppRoot(projectRoot) {
21
61
  // Bump to v2 to prevent the CLI from setting the variable anymore.
22
62
  // TODO: Bump to v3 to revert back to the CLI setting the variable again, but with custom value
@@ -26,7 +66,7 @@ function getExpoRouterAppRoot(projectRoot) {
26
66
  }
27
67
  const routerEntry = resolveFrom.silent(projectRoot, "expo-router/entry");
28
68
 
29
- const { exp } = getConfig(projectRoot);
69
+ const { exp } = getConfigMemo(projectRoot);
30
70
  const customSrc = exp.extra?.router?.unstable_src || "./app";
31
71
  const isAbsolute = customSrc.startsWith("/");
32
72
  // It doesn't matter if the app folder exists.
@@ -46,6 +86,7 @@ module.exports = function (api) {
46
86
  const getRelPath = (state) =>
47
87
  "./" + nodePath.relative(state.file.opts.root, state.filename);
48
88
 
89
+ const platform = api.caller((caller) => caller?.platform);
49
90
  return {
50
91
  name: "expo-router",
51
92
  visitor: {
@@ -123,6 +164,19 @@ module.exports = function (api) {
123
164
  return;
124
165
  }
125
166
 
167
+ // Expose the app route import mode.
168
+ if (
169
+ t.isIdentifier(parent.node.property, {
170
+ name: "EXPO_ROUTER_IMPORT_MODE",
171
+ }) &&
172
+ !parent.parentPath.isAssignmentExpression()
173
+ ) {
174
+ parent.replaceWith(
175
+ t.stringLiteral(getExpoRouterImportMode(projectRoot, platform))
176
+ );
177
+ return;
178
+ }
179
+
126
180
  if (
127
181
  !t.isIdentifier(parent.node.property, {
128
182
  name: "EXPO_ROUTER_APP_ROOT",
@@ -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;AAS5D,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;AA8MD;;;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,CAYlB;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,CAgBlB"}
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;AAS5D,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,CAYlB;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,CAgBlB"}
@@ -139,7 +139,7 @@ export declare const Tabs: React.ForwardRefExoticComponent<Omit<Omit<import("@re
139
139
  show?: import("@react-navigation/bottom-tabs/lib/typescript/src/types").TabBarVisibilityAnimationConfig | undefined;
140
140
  hide?: import("@react-navigation/bottom-tabs/lib/typescript/src/types").TabBarVisibilityAnimationConfig | undefined;
141
141
  } | undefined;
142
- tabBarStyle?: false | import("react-native").RegisteredStyle<import("react-native").ViewStyle> | import("react-native").Animated.Value | import("react-native").Animated.AnimatedInterpolation<string | number> | import("react-native").Animated.WithAnimatedObject<import("react-native").ViewStyle> | import("react-native").Animated.WithAnimatedArray<import("react-native").Falsy | import("react-native").ViewStyle | import("react-native").RegisteredStyle<import("react-native").ViewStyle> | import("react-native").RecursiveArray<import("react-native").Falsy | import("react-native").ViewStyle | import("react-native").RegisteredStyle<import("react-native").ViewStyle>> | readonly (import("react-native").Falsy | import("react-native").ViewStyle | import("react-native").RegisteredStyle<import("react-native").ViewStyle>)[]> | null | undefined;
142
+ tabBarStyle?: false | import("react-native").Animated.Value | import("react-native").RegisteredStyle<import("react-native").ViewStyle> | import("react-native").Animated.AnimatedInterpolation<string | number> | import("react-native").Animated.WithAnimatedObject<import("react-native").ViewStyle> | import("react-native").Animated.WithAnimatedArray<import("react-native").Falsy | import("react-native").ViewStyle | import("react-native").RegisteredStyle<import("react-native").ViewStyle> | import("react-native").RecursiveArray<import("react-native").Falsy | import("react-native").ViewStyle | import("react-native").RegisteredStyle<import("react-native").ViewStyle>> | readonly (import("react-native").Falsy | import("react-native").ViewStyle | import("react-native").RegisteredStyle<import("react-native").ViewStyle>)[]> | null | undefined;
143
143
  tabBarBackground?: (() => React.ReactNode) | undefined;
144
144
  lazy?: boolean | undefined;
145
145
  header?: ((props: import("@react-navigation/bottom-tabs").BottomTabHeaderProps) => React.ReactNode) | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"useTutorial.d.ts","sourceRoot":"","sources":["../../src/onboard/useTutorial.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAa1C,yIAAyI;AACzI,wBAAgB,WAAW,CAAC,OAAO,EAAE,cAAc,OAwBlD"}
1
+ {"version":3,"file":"useTutorial.d.ts","sourceRoot":"","sources":["../../src/onboard/useTutorial.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAa1C,yIAAyI;AACzI,wBAAgB,WAAW,CAAC,OAAO,EAAE,cAAc,OA4BlD"}
@@ -1 +1 @@
1
- {"version":3,"file":"useScreens.d.ts","sourceRoot":"","sources":["../src/useScreens.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,OAAO,EAGL,SAAS,EAGV,MAAM,SAAS,CAAC;AAIjB,MAAM,MAAM,WAAW,CACrB,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,IACxD;IACF,4DAA4D;IAC5D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,aAAa,CAAC,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;KAAE,CAAC;IACvC,OAAO,CAAC,EAAE,QAAQ,CAAC;IAGnB,SAAS,CAAC,EAAE,GAAG,CAAC;CACjB,CAAC;AA8DF;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,KAAK,CAAC,SAAS,EAAE,CAUxE;AAMD,mFAAmF;AACnF,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,SAAS,+GAgD1D;AAED,oGAAoG;AACpG,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,IAAI,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC;;sCA0B5C"}
1
+ {"version":3,"file":"useScreens.d.ts","sourceRoot":"","sources":["../src/useScreens.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,OAAO,EAIL,SAAS,EAGV,MAAM,SAAS,CAAC;AAMjB,MAAM,MAAM,WAAW,CACrB,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,IACxD;IACF,4DAA4D;IAC5D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,aAAa,CAAC,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;KAAE,CAAC;IACvC,OAAO,CAAC,EAAE,QAAQ,CAAC;IAGnB,SAAS,CAAC,EAAE,GAAG,CAAC;CACjB,CAAC;AA8DF;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,KAAK,CAAC,SAAS,EAAE,CAUxE;AA6BD,mFAAmF;AACnF,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,SAAS,+GA2E1D;AAED,oGAAoG;AACpG,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,IAAI,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC;;sCA0B5C"}
@@ -0,0 +1,3 @@
1
+ /// <reference types="react" />
2
+ export declare function EmptyRoute(): JSX.Element;
3
+ //# sourceMappingURL=EmptyRoute.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EmptyRoute.d.ts","sourceRoot":"","sources":["../../src/views/EmptyRoute.tsx"],"names":[],"mappings":";AAKA,wBAAgB,UAAU,gBAUzB"}
@@ -0,0 +1,6 @@
1
+ /// <reference types="react" />
2
+ import { RouteNode } from "../Route";
3
+ export declare function SuspenseFallback({ route }: {
4
+ route: RouteNode;
5
+ }): JSX.Element;
6
+ //# sourceMappingURL=SuspenseFallback.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SuspenseFallback.d.ts","sourceRoot":"","sources":["../../src/views/SuspenseFallback.tsx"],"names":[],"mappings":";AAGA,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAErC,wBAAgB,gBAAgB,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,SAAS,CAAA;CAAE,eAM/D"}
@@ -0,0 +1,11 @@
1
+ import React from "react";
2
+ export declare const CODE_FONT: string;
3
+ export declare function ToastWrapper({ children }: {
4
+ children: React.ReactNode;
5
+ }): JSX.Element;
6
+ export declare function Toast({ children, filename, warning, }: {
7
+ children: React.ReactNode;
8
+ filename?: string;
9
+ warning?: boolean;
10
+ }): JSX.Element;
11
+ //# sourceMappingURL=Toast.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Toast.d.ts","sourceRoot":"","sources":["../../src/views/Toast.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAC;AAI1B,eAAO,MAAM,SAAS,QAIpB,CAAC;AAeH,wBAAgB,YAAY,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAAE,eASvE;AAED,wBAAgB,KAAK,CAAC,EACpB,QAAQ,EACR,QAAQ,EACR,OAAO,GACR,EAAE;IACD,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,eAyBA"}
package/entry.js CHANGED
@@ -2,10 +2,14 @@ import "@expo/metro-runtime";
2
2
 
3
3
  import { ExpoRoot } from "expo-router";
4
4
  import Head from "expo-router/head";
5
+ import { renderRootComponent } from "expo-router/src/renderRootComponent";
5
6
 
6
- import { renderRootComponent } from "./src/renderRootComponent";
7
-
8
- const ctx = require.context(process.env.EXPO_ROUTER_APP_ROOT);
7
+ const ctx = require.context(
8
+ process.env.EXPO_ROUTER_APP_ROOT,
9
+ true,
10
+ /.*/,
11
+ process.env.EXPO_ROUTER_IMPORT_MODE
12
+ );
9
13
 
10
14
  // Must be exported or Fast Refresh won't update the context
11
15
  export function App() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-router",
3
- "version": "1.4.3",
3
+ "version": "1.5.0",
4
4
  "main": "src/index.tsx",
5
5
  "types": "build/index.d.ts",
6
6
  "files": [
@@ -92,7 +92,7 @@
92
92
  },
93
93
  "dependencies": {
94
94
  "@bacons/react-views": "^1.1.3",
95
- "@expo/metro-runtime": "2.0.3",
95
+ "@expo/metro-runtime": "2.0.4",
96
96
  "@radix-ui/react-slot": "^1.0.0",
97
97
  "@react-navigation/bottom-tabs": "~6.5.7",
98
98
  "@react-navigation/native": "~6.1.6",
package/src/getRoutes.ts CHANGED
@@ -271,11 +271,19 @@ function contextModuleToFileNodes(
271
271
  // In development, check if the file exports a default component
272
272
  // this helps keep things snappy when creating files. In production we load all screens lazily.
273
273
  try {
274
- if (!contextModule(key)?.default) {
275
- return null;
274
+ if (process.env.NODE_ENV === "development") {
275
+ // If the user has set the `EXPO_ROUTER_IMPORT_MODE` to `sync` then we should
276
+ // filter the missing routes.
277
+ if (process.env.EXPO_ROUTER_IMPORT_MODE === "sync") {
278
+ if (!contextModule(key)?.default) {
279
+ return null;
280
+ }
281
+ }
276
282
  }
277
283
  const node: FileNode = {
278
- loadRoute: () => contextModule(key),
284
+ loadRoute() {
285
+ return contextModule(key);
286
+ },
279
287
  normalizedName: getNameFromFilePath(key),
280
288
  contextKey: key,
281
289
  };
@@ -1,10 +1,10 @@
1
- import React, { useMemo } from "react";
1
+ import { useMemo, ComponentType } from "react";
2
2
 
3
3
  import { RequireContext } from "../types";
4
4
 
5
5
  function isFunctionOrReactComponent(
6
6
  Component: any
7
- ): Component is React.ComponentType {
7
+ ): Component is ComponentType {
8
8
  return (
9
9
  !!Component &&
10
10
  (typeof Component === "function" ||
@@ -23,14 +23,18 @@ export function useTutorial(context: RequireContext) {
23
23
  const keys = useMemo(() => context.keys(), [context]);
24
24
  // eslint-disable-next-line react-hooks/rules-of-hooks
25
25
  const hasAnyValidComponent = useMemo(() => {
26
- for (const key of keys) {
27
- // NOTE(EvanBacon): This should only ever occur in development as it breaks lazily loading.
28
- const component = context(key)?.default;
29
- if (isFunctionOrReactComponent(component)) {
30
- return true;
26
+ if (process.env.EXPO_ROUTER_IMPORT_MODE === "sync") {
27
+ for (const key of keys) {
28
+ // NOTE(EvanBacon): This should only ever occur in development as it breaks lazily loading.
29
+ const component = context(key)?.default;
30
+ if (isFunctionOrReactComponent(component)) {
31
+ return true;
32
+ }
31
33
  }
34
+ return false;
32
35
  }
33
- return false;
36
+ return !!context.keys().length;
37
+ // return false;
34
38
  }, [keys]);
35
39
 
36
40
  if (hasAnyValidComponent) {
@@ -3,12 +3,15 @@ import React from "react";
3
3
  import { LocationProvider } from "./LocationProvider";
4
4
  import {
5
5
  DynamicConvention,
6
+ LoadedRoute,
6
7
  Route,
7
8
  RouteNode,
8
9
  sortRoutesWithInitial,
9
10
  useRouteNode,
10
11
  } from "./Route";
11
12
  import { Screen } from "./primitives";
13
+ import { EmptyRoute } from "./views/EmptyRoute";
14
+ import { SuspenseFallback } from "./views/SuspenseFallback";
12
15
  import { Try } from "./views/Try";
13
16
 
14
17
  export type ScreenProps<
@@ -103,6 +106,29 @@ export function useSortedScreens(order: ScreenProps[]): React.ReactNode[] {
103
106
  );
104
107
  }
105
108
 
109
+ function fromImport({ ErrorBoundary, ...component }: LoadedRoute) {
110
+ if (ErrorBoundary) {
111
+ return {
112
+ default: React.forwardRef((props: any, ref: any) => {
113
+ const children = React.createElement(component.default, {
114
+ ...props,
115
+ ref,
116
+ });
117
+ return <Try catch={ErrorBoundary}>{children}</Try>;
118
+ }),
119
+ };
120
+ }
121
+ return { default: component.default || EmptyRoute };
122
+ }
123
+
124
+ function fromLoadedRoute(res: LoadedRoute) {
125
+ if (!(res instanceof Promise)) {
126
+ return fromImport(res);
127
+ }
128
+
129
+ return res.then(fromImport);
130
+ }
131
+
106
132
  // TODO: Maybe there's a more React-y way to do this?
107
133
  // Without this store, the process enters a recursive loop.
108
134
  const qualifiedStore = new WeakMap<RouteNode, React.ComponentType<any>>();
@@ -113,7 +139,48 @@ export function getQualifiedRouteComponent(value: RouteNode) {
113
139
  return qualifiedStore.get(value)!;
114
140
  }
115
141
 
116
- const { default: Component, ErrorBoundary } = value.loadRoute();
142
+ let getLoadable: (props: any, ref: any) => JSX.Element;
143
+
144
+ // TODO: This ensures sync doesn't use React.lazy, but it's not ideal.
145
+ if (process.env.EXPO_ROUTER_IMPORT_MODE === "sync") {
146
+ const SyncComponent = React.forwardRef((props, ref) => {
147
+ const res = value.loadRoute();
148
+ const Component = fromImport(res).default;
149
+ return <Component {...props} ref={ref} />;
150
+ });
151
+
152
+ getLoadable = (props: any, ref: any) => (
153
+ <SyncComponent
154
+ {...{
155
+ ...props,
156
+ ref,
157
+ // Expose the template segment path, e.g. `(home)`, `[foo]`, `index`
158
+ // the intention is to make it possible to deduce shared routes.
159
+ segment: value.route,
160
+ }}
161
+ />
162
+ );
163
+ } else {
164
+ const AsyncComponent = React.lazy(async () => {
165
+ const res = value.loadRoute();
166
+ return fromLoadedRoute(res) as Promise<{
167
+ default: React.ComponentType<any>;
168
+ }>;
169
+ });
170
+ getLoadable = (props: any, ref: any) => (
171
+ <React.Suspense fallback={<SuspenseFallback route={value} />}>
172
+ <AsyncComponent
173
+ {...{
174
+ ...props,
175
+ ref,
176
+ // Expose the template segment path, e.g. `(home)`, `[foo]`, `index`
177
+ // the intention is to make it possible to deduce shared routes.
178
+ segment: value.route,
179
+ }}
180
+ />
181
+ </React.Suspense>
182
+ );
183
+ }
117
184
 
118
185
  const QualifiedRoute = React.forwardRef(
119
186
  (
@@ -128,32 +195,18 @@ export function getQualifiedRouteComponent(value: RouteNode) {
128
195
  }: any,
129
196
  ref: any
130
197
  ) => {
131
- const children = React.createElement(Component, {
132
- ...props,
133
- ref,
134
- // Expose the template segment path, e.g. `(home)`, `[foo]`, `index`
135
- // the intention is to make it possible to deduce shared routes.
136
- segment: value.route,
137
- });
138
-
139
- const errorBoundary = React.useMemo(() => {
140
- if (ErrorBoundary) {
141
- return <Try catch={ErrorBoundary}>{children}</Try>;
142
- }
143
- return children;
144
- }, [ErrorBoundary, children]);
198
+ const loadable = getLoadable(props, ref);
145
199
 
146
200
  return (
147
201
  <LocationProvider>
148
- <Route node={value}>{errorBoundary}</Route>
202
+ <Route node={value}>{loadable}</Route>
149
203
  </LocationProvider>
150
204
  );
151
205
  }
152
206
  );
153
207
 
154
- QualifiedRoute.displayName = `Route(${
155
- Component.displayName || Component.name || value.route
156
- })`;
208
+ QualifiedRoute.displayName = `Route(${value.route})`;
209
+
157
210
  qualifiedStore.set(value, QualifiedRoute);
158
211
  return QualifiedRoute;
159
212
  }
@@ -0,0 +1,16 @@
1
+ import React from "react";
2
+
3
+ import { Toast, ToastWrapper } from "./Toast";
4
+ import { useRouteNode } from "../Route";
5
+
6
+ export function EmptyRoute() {
7
+ const route = useRouteNode();
8
+
9
+ return (
10
+ <ToastWrapper>
11
+ <Toast warning filename={route?.contextKey}>
12
+ Missing default export
13
+ </Toast>
14
+ </ToastWrapper>
15
+ );
16
+ }
@@ -0,0 +1,12 @@
1
+ import React from "react";
2
+
3
+ import { Toast, ToastWrapper } from "./Toast";
4
+ import { RouteNode } from "../Route";
5
+
6
+ export function SuspenseFallback({ route }: { route: RouteNode }) {
7
+ return (
8
+ <ToastWrapper>
9
+ <Toast filename={route?.contextKey}>Bundling...</Toast>
10
+ </ToastWrapper>
11
+ );
12
+ }
@@ -0,0 +1,99 @@
1
+ import { Image, StyleSheet, Text, View } from "@bacons/react-views";
2
+ import { BottomTabBarHeightContext } from "@react-navigation/bottom-tabs";
3
+ import React from "react";
4
+ import { ActivityIndicator, Animated, Platform } from "react-native";
5
+ import { SafeAreaView } from "react-native-safe-area-context";
6
+
7
+ export const CODE_FONT = Platform.select({
8
+ default: "Courier",
9
+ ios: "Courier New",
10
+ android: "monospace",
11
+ });
12
+
13
+ function useFadeIn() {
14
+ // Returns a React Native Animated value for fading in
15
+ const [value] = React.useState(() => new Animated.Value(0));
16
+ React.useEffect(() => {
17
+ Animated.timing(value, {
18
+ toValue: 1,
19
+ duration: 200,
20
+ useNativeDriver: true,
21
+ }).start();
22
+ }, []);
23
+ return value;
24
+ }
25
+
26
+ export function ToastWrapper({ children }: { children: React.ReactNode }) {
27
+ const inTabBar = React.useContext(BottomTabBarHeightContext);
28
+ const Wrapper = inTabBar ? View : SafeAreaView;
29
+
30
+ return (
31
+ <Wrapper collapsable={false} style={{ flex: 1 }}>
32
+ {children}
33
+ </Wrapper>
34
+ );
35
+ }
36
+
37
+ export function Toast({
38
+ children,
39
+ filename,
40
+ warning,
41
+ }: {
42
+ children: React.ReactNode;
43
+ filename?: string;
44
+ warning?: boolean;
45
+ }) {
46
+ const filenamePretty = React.useMemo(() => {
47
+ if (!filename) return undefined;
48
+ return "app" + filename.replace(/^\./, "");
49
+ }, [filename]);
50
+ const value = useFadeIn();
51
+ return (
52
+ <View style={styles.container}>
53
+ <Animated.View style={[styles.toast, { opacity: value }]}>
54
+ {!warning && <ActivityIndicator color="white" />}
55
+ {warning && (
56
+ <Image
57
+ source={require("expo-router/assets/error.png")}
58
+ style={styles.icon}
59
+ />
60
+ )}
61
+ <View style={{ marginLeft: 8 }}>
62
+ <Text style={styles.text}>{children}</Text>
63
+ {filenamePretty && (
64
+ <Text style={styles.filename}>{filenamePretty}</Text>
65
+ )}
66
+ </View>
67
+ </Animated.View>
68
+ </View>
69
+ );
70
+ }
71
+
72
+ const styles = StyleSheet.create({
73
+ container: {
74
+ backgroundColor: "transparent",
75
+ flex: 1,
76
+ },
77
+ icon: { width: 20, height: 20, resizeMode: "contain" },
78
+ toast: {
79
+ alignItems: "center",
80
+ borderWidth: 1,
81
+ borderColor: "rgba(255,255,255,0.2)",
82
+ flexDirection: "row",
83
+ position: "absolute",
84
+ bottom: 8,
85
+ left: 8,
86
+ paddingVertical: 8,
87
+ paddingHorizontal: 12,
88
+ borderRadius: 4,
89
+ backgroundColor: "black",
90
+ },
91
+ text: { color: "white", fontSize: 16 },
92
+ filename: {
93
+ fontFamily: CODE_FONT,
94
+ opacity: 0.8,
95
+ color: "white",
96
+ fontSize: 12,
97
+ },
98
+ code: { fontFamily: CODE_FONT },
99
+ });