diff --git a/__tests__/app-config.test.ts b/__tests__/app-config.test.ts new file mode 100644 index 000000000..c7085e4b5 --- /dev/null +++ b/__tests__/app-config.test.ts @@ -0,0 +1,74 @@ +import type { ConfigContext, ExpoConfig } from '@expo/config'; + +type AppConfigFn = (context: ConfigContext) => ExpoConfig; + +const loadConfigFor = (appEnv: string | undefined): ExpoConfig => { + if (appEnv === undefined) { + delete process.env.APP_ENV; + } else { + process.env.APP_ENV = appEnv; + } + let appConfig: AppConfigFn | undefined; + jest.isolateModules(() => { + appConfig = require('../app.config').default; + }); + if (!appConfig) { + throw new Error('app.config.ts could not be loaded'); + } + return appConfig({ config: {} } as ConfigContext); +}; + +const getExpoRouterOptions = (config: ExpoConfig) => { + const entry = config.plugins?.find((plugin) => + Array.isArray(plugin) + ? plugin[0] === 'expo-router' + : plugin === 'expo-router', + ); + if (!Array.isArray(entry)) { + throw new Error( + `Expected the expo-router plugin to be configured as ['expo-router', options], received ${JSON.stringify(entry)}`, + ); + } + return entry[1]; +}; + +describe('app.config expo-router sitemap', () => { + const originalAppEnv = process.env.APP_ENV; + + afterEach(() => { + if (originalAppEnv === undefined) { + delete process.env.APP_ENV; + } else { + process.env.APP_ENV = originalAppEnv; + } + }); + + it('disables the sitemap route in production', () => { + const config = loadConfigFor('production'); + + expect(getExpoRouterOptions(config)).toEqual({ sitemap: false }); + }); + + it.each(['development', 'qa', 'staging'])( + 'keeps the sitemap route in %s', + (appEnv) => { + const config = loadConfigFor(appEnv); + + expect(getExpoRouterOptions(config)).toEqual({ sitemap: true }); + }, + ); + + it('keeps the sitemap route when APP_ENV is unset (defaults to development)', () => { + const config = loadConfigFor(undefined); + + expect(getExpoRouterOptions(config)).toEqual({ sitemap: true }); + }); + + it('fails loudly when the expo-router entry has no options', () => { + const config = { plugins: ['expo-router'] } as ExpoConfig; + + expect(() => getExpoRouterOptions(config)).toThrow( + /Expected the expo-router plugin/, + ); + }); +}); diff --git a/agent_docs/commands.md b/agent_docs/commands.md index 28d8ff501..334bd9783 100644 --- a/agent_docs/commands.md +++ b/agent_docs/commands.md @@ -64,6 +64,19 @@ Builds are created with Expo Application Services. Always set `APP_ENV` via the --- +## Update (EAS) + +OTA updates are published with `eas update`. Always use these scripts so `APP_ENV` matches the target channel; publishing without it falls back to `development` config (e.g. re-enables the `/_sitemap` route in production). Pass a message with `--message "..."`. + +| Command | Description | +| ----------------------------- | ---------------------------------- | +| `pnpm run update:development` | EAS update — `development` channel | +| `pnpm run update:qa` | EAS update — `qa` channel | +| `pnpm run update:staging` | EAS update — `staging` channel | +| `pnpm run update:production` | EAS update — `production` channel | + +--- + ## Setup | Command | Description | diff --git a/app.config.ts b/app.config.ts index 6831bb657..0b7b4a169 100644 --- a/app.config.ts +++ b/app.config.ts @@ -86,7 +86,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({ ], 'expo-dev-client', 'expo-localization', - 'expo-router', + ['expo-router', { sitemap: Env.APP_ENV !== 'production' }], ['app-icon-badge', appIconBadgeConfig], ['react-native-edge-to-edge'], ], diff --git a/docs/src/assets/sitemap-screen.png b/docs/src/assets/sitemap-screen.png new file mode 100644 index 000000000..c1f716929 Binary files /dev/null and b/docs/src/assets/sitemap-screen.png differ diff --git a/docs/src/content/docs/guides/navigation.mdx b/docs/src/content/docs/guides/navigation.mdx index 36cd82a8e..7294f602c 100644 --- a/docs/src/content/docs/guides/navigation.mdx +++ b/docs/src/content/docs/guides/navigation.mdx @@ -22,4 +22,24 @@ Here is a simple example of the tabs layout. +## Sitemap route + +Expo Router injects a [`/_sitemap`](https://docs.expo.dev/router/reference/sitemap/) route that lists every route in the app, which is handy for debugging. The template removes this in-app route browser from production builds: the `expo-router` plugin in `app.config.ts` is configured with `sitemap: Env.APP_ENV !== 'production'`. This does not hide the route names themselves, which still ship in the JS bundle and the typed-routes output. On `development`, `qa` and `staging` builds you can open `://_sitemap` (or `/_sitemap` on web); on `production` builds that path is handled by the template's catch-all route, `src/app/[...messing].tsx`. + +If you remove that catch-all route, unmatched paths fall back to Expo Router's built-in not-found screen, which renders a hard-coded link to `/_sitemap`. In production that link leads nowhere, so keep a custom catch-all (or `+not-found`) route if you disable the sitemap. + +Here is what the sitemap screen looks like (captured on the web build, `development` environment): + +![Expo Router sitemap screen listing the app routes, such as sign-in, sign-up, onboarding and feed, followed by a System Information card showing development mode](../../../assets/sitemap-screen.png) + +To change this behavior, update the `sitemap` option of the `expo-router` plugin in `app.config.ts` (set it to `true` to always include the route, or `false` to always remove it). + +Because the option is read from `APP_ENV` when the app config is evaluated, OTA updates must also be published with `APP_ENV` set. `env.js` falls back to `development` when it is unset, so an `eas update` published to the `production` channel without `APP_ENV=production` would re-enable `/_sitemap` in the production app. Use the per-environment scripts, which set `APP_ENV` and target the matching channel: + +```bash +pnpm run update:production --message "Fix login copy" +``` + +`update:development`, `update:qa` and `update:staging` work the same way for the other channels. + Make sure to check the official docs for more information and examples about [expo-router](https://docs.expo.dev/router/introduction/). diff --git a/package.json b/package.json index 283cf1efd..5247a374d 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "build:development:ios": "cross-env APP_ENV=development EXPO_NO_DOTENV=1 eas build --profile development --platform ios", "build:development:web": "cross-env APP_ENV=development EXPO_NO_DOTENV=1 expo export -c --platform web", "submit:development:mobile": "cross-env APP_ENV=development eas submit --profile development", + "update:development": "cross-env APP_ENV=development EXPO_NO_DOTENV=1 eas update --channel development", "start:qa": "cross-env APP_ENV=qa pnpm run start", "prebuild:qa": "cross-env APP_ENV=qa pnpm run prebuild", "android:qa": "cross-env APP_ENV=qa pnpm run android", @@ -24,6 +25,7 @@ "build:qa:ios": "cross-env APP_ENV=qa EXPO_NO_DOTENV=1 eas build --profile qa --platform ios", "build:qa:web": "cross-env APP_ENV=qa EXPO_NO_DOTENV=1 expo export -c --platform web", "submit:qa:mobile": "cross-env APP_ENV=qa eas submit --profile qa", + "update:qa": "cross-env APP_ENV=qa EXPO_NO_DOTENV=1 eas update --channel qa", "start:staging": "cross-env APP_ENV=staging pnpm run start", "prebuild:staging": "cross-env APP_ENV=staging pnpm run prebuild", "android:staging": "cross-env APP_ENV=staging pnpm run android", @@ -33,6 +35,7 @@ "build:staging:ios": "cross-env APP_ENV=staging EXPO_NO_DOTENV=1 eas build --profile staging --platform ios", "build:staging:web": "cross-env APP_ENV=staging EXPO_NO_DOTENV=1 expo export -c --platform web", "submit:staging:mobile": "cross-env APP_ENV=staging eas submit --profile staging", + "update:staging": "cross-env APP_ENV=staging EXPO_NO_DOTENV=1 eas update --channel staging", "start:production": "cross-env APP_ENV=production pnpm run start", "prebuild:production": "cross-env APP_ENV=production pnpm run prebuild", "android:production": "cross-env APP_ENV=production pnpm run android", @@ -42,6 +45,7 @@ "build:production:ios": "cross-env APP_ENV=production EXPO_NO_DOTENV=1 eas build --profile production --platform ios", "build:production:web": "cross-env APP_ENV=production EXPO_NO_DOTENV=1 expo export -c --platform web", "submit:production:mobile": "cross-env APP_ENV=production eas submit --profile production", + "update:production": "cross-env APP_ENV=production EXPO_NO_DOTENV=1 eas update --channel production", "credentials:ios": "eas credentials --platform ios", "credentials:android": "eas credentials --platform android", "prepare": "husky",