Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,14 @@ export const MapsScreens = Platform.select({
return optionalRequire(() => require('./apple/MapsPolylineScreen'));
},
},
{
name: 'Geodesic polyline',
route: 'expo-maps/geodesic-polyline',
options: {},
getComponent() {
return optionalRequire(() => require('./apple/MapsGeodesicPolylineScreen'));
},
},
{
name: 'Polygon',
route: 'expo-maps/polygon',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { AppleMaps } from 'expo-maps';
import { useState } from 'react';
import { Alert, StyleSheet, Text, TouchableOpacity, View } from 'react-native';

import { flightPaths } from '../data';

const colors = ['#e74c3c', '#2ecc71', '#3498db'];

export default function MapsGeodesicPolylineScreen() {
const [showGeodesic, setShowGeodesic] = useState(true);
const [showStraight, setShowStraight] = useState(true);

const geodesicPolylines = flightPaths.map((path, i) => ({
id: `geodesic-${path.name}`,
coordinates: path.coordinates,
color: colors[i % colors.length],
width: 3,
contourStyle: AppleMaps.ContourStyle.GEODESIC,
}));

const straightPolylines = flightPaths.map((path, i) => ({
id: `straight-${path.name}`,
coordinates: path.coordinates,
color: colors[i % colors.length],
width: 2,
contourStyle: AppleMaps.ContourStyle.STRAIGHT,
}));

const polylines = [
...(showGeodesic ? geodesicPolylines : []),
...(showStraight ? straightPolylines : []),
];

return (
<View style={styles.container}>
<AppleMaps.View
style={styles.map}
cameraPosition={{
coordinates: { latitude: 45, longitude: -30 },
zoom: 3,
}}
properties={{
polylineTapThreshold: 50_000,
}}
onPolylineClick={(event) => {
Alert.alert('Polyline clicked', JSON.stringify(event, null, 2));
}}
polylines={polylines}
/>
<View style={styles.controls}>
<TouchableOpacity
style={[styles.button, showGeodesic && styles.buttonActive]}
onPress={() => setShowGeodesic((v) => !v)}>
<Text style={styles.buttonText}>Geodesic {showGeodesic ? 'ON' : 'OFF'}</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, showStraight && styles.buttonActive]}
onPress={() => setShowStraight((v) => !v)}>
<Text style={styles.buttonText}>Straight {showStraight ? 'ON' : 'OFF'}</Text>
</TouchableOpacity>
</View>
<View style={styles.legend}>
<Text style={styles.legendTitle}>Flight paths:</Text>
{flightPaths.map((path, i) => (
<Text key={path.name} style={[styles.legendItem, { color: colors[i % colors.length] }]}>
{path.name}
</Text>
))}
<Text style={styles.legendHint}>
{showGeodesic && showStraight
? 'Geodesic lines curve along great circles; straight lines cut across the projection'
: showGeodesic
? 'Showing geodesic (curved) lines only'
: showStraight
? 'Showing straight lines only'
: 'Toggle lines above'}
</Text>
</View>
</View>
);
}

const styles = StyleSheet.create({
container: { flex: 1 },
map: { flex: 1 },
controls: {
position: 'absolute',
top: 60,
right: 12,
gap: 8,
},
button: {
backgroundColor: 'rgba(0,0,0,0.5)',
paddingHorizontal: 12,
paddingVertical: 8,
borderRadius: 8,
},
buttonActive: {
backgroundColor: 'rgba(0,0,0,0.8)',
},
buttonText: {
color: '#fff',
fontSize: 13,
fontWeight: '600',
},
legend: {
position: 'absolute',
bottom: 40,
left: 12,
right: 12,
backgroundColor: 'rgba(255,255,255,0.9)',
borderRadius: 10,
padding: 12,
},
legendTitle: {
fontWeight: '700',
marginBottom: 4,
},
legendItem: {
fontWeight: '600',
fontSize: 13,
},
legendHint: {
marginTop: 6,
fontSize: 12,
color: '#666',
},
});
20 changes: 20 additions & 0 deletions apps/native-component-list/src/screens/ExpoMaps/data.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,23 @@
// Transatlantic flight paths for geodesic polyline testing.
// Long east-west routes at high latitudes show dramatic geodesic curvature
// (the great circle arc bows north over the Atlantic/Arctic).
export const flightPaths = [
{
name: 'Prague → NYC',
coordinates: [
{ latitude: 50.1008, longitude: 14.26 }, // PRG
{ latitude: 40.6413, longitude: -73.7781 }, // JFK
],
},
{
name: 'London → Miami',
coordinates: [
{ latitude: 51.47, longitude: -0.4543 }, // LHR
{ latitude: 25.7959, longitude: -80.287 }, // MIA
],
},
];

export const polylineCoordinates = [
{
latitude: 46.334295,
Expand Down
11 changes: 11 additions & 0 deletions apps/native-component-list/src/screens/UI/SectionScreen.ios.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ export default function SectionScreen() {
<Text>This section uses the title prop</Text>
<Text>Simple and clean</Text>
</Section>
<Section title="Title with Footer" footer={<Text>This footer should be visible</Text>}>
<Text>This section has both a title and a footer</Text>
</Section>
<Section
header={<Text>Header with Footer</Text>}
footer={<Text>Footer with custom header</Text>}>
<Text>This section uses header and footer props</Text>
</Section>
<Section footer={<Text>Footer without header</Text>}>
<Text>This section only has a footer</Text>
</Section>
</List>
</Host>
);
Expand Down
40 changes: 40 additions & 0 deletions apps/router-e2e/__e2e__/server-loader/app/(group)/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useLoaderData, useLocalSearchParams, usePathname } from 'expo-router';
import { Suspense } from 'react';

import { Loading } from '../../components/Loading';
import { SiteLinks, SiteLink } from '../../components/SiteLink';
import { Table, TableRow } from '../../components/Table';

export async function loader() {
return Promise.resolve({
data: 'grouped-index',
});
}

export default function GroupedIndexRoute() {
return (
<Suspense fallback={<Loading />}>
<GroupedIndexScreen />
</Suspense>
);
}

const GroupedIndexScreen = () => {
const pathname = usePathname();
const localParams = useLocalSearchParams();
const data = useLoaderData<typeof loader>();

return (
<>
<Table>
<TableRow label="Pathname" value={pathname} testID="pathname-result" />
<TableRow label="Local Params" value={localParams} testID="localparams-result" />
<TableRow label="Loader Data" value={data} testID="loader-result" />
</Table>

<SiteLinks>
<SiteLink href="/">Go to Index</SiteLink>
</SiteLinks>
</>
);
};
1 change: 1 addition & 0 deletions apps/router-e2e/__e2e__/server-loader/app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const IndexScreen = () => {
<SiteLink href="/posts/static-post-1">Go to static Post 1</SiteLink>
<SiteLink href="/posts/static-post-2">Go to static Post 2</SiteLink>
<SiteLink href="/error">Go to Error</SiteLink>
<SiteLink href="/(group)">Go to Grouped Index</SiteLink>
</SiteLinks>
</>
);
Expand Down
1 change: 1 addition & 0 deletions apps/router-e2e/__e2e__/server-loader/workerd/config.capnp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const server :Workerd.Worker = (
(name = "_expo/loaders/nullish/[value].js", commonJsModule = embed "_expo/loaders/nullish/[value].js"),
(name = "_expo/loaders/request.js", commonJsModule = embed "_expo/loaders/request.js"),
(name = "_expo/loaders/response.js", commonJsModule = embed "_expo/loaders/response.js"),
(name = "_expo/loaders/(group)/index.js", commonJsModule = embed "_expo/loaders/(group)/index.js"),
],
bindings = [
(name = "TEST_SECRET_RUNTIME_KEY", text = "runtime-secret-value"),
Expand Down
13 changes: 13 additions & 0 deletions docs/.vale.ini
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,16 @@ CommentDelimiters = {/*, */}
# Ignore code surrounded by backticks or plus sign, parameters defaults, URLs and so on.
BlockIgnores = (?s)<Terminal.*?/>,
TokenIgnores = (\x60[^\n\x60]+\x60), ([^\n]+=[^\n]*), (\+[^\n]+\+), (http[^\n]+\[), .github, .gitlab, vscode-, Signing & Capabilities, eas.json, eas-json, .yarn+, yarn: "# yarn", eas+, eas-cli, npx+, Build & deploy, OAuth & Permissions, Languages & Frameworks, typescript, Privacy & Security, Certificates, IDs & Profiles, application/javascript, Motion & Orientation Access, Show devices and ask me again, my-app, Still having trouble?, "my app runs well locally by crashes immediately when I run a build", my-app, MY_CUSTOM_API_KEY, withMyApiKey, My App, com.my., myFunction, my app, my-plugin, my-module, 'Hello from my TypeScript function!', my-scheme, my-root, Change my environment variables, my custom plugin, my, cocoapods, javascript, Ink & Switch, macos-medium, macos-large,

# Ignore internal test fixtures and generated output.
[pages/internal/**]
Vale = NO
BasedOnStyles =

[out/**]
Vale = NO
BasedOnStyles =

[pages/versions/latest/**]
Vale = NO
BasedOnStyles =
4 changes: 2 additions & 2 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
"export-prep": "node ./scripts/export-prep.js",
"export-server": "wrangler pages dev --port 8000",
"test:worker": "node --experimental-strip-types scripts/test-worker.ts",
"lint": "node scripts/lint.js --max-warnings 0",
"lint": "NODE_OPTIONS='--no-warnings' node scripts/lint.js --max-warnings 0",
"lint-links": "remark -u validate-links ./pages",
"lint-prose": "yarn vale --glob='!pages/versions/latest/**' .",
"lint-prose": "yarn vale --config='.vale.ini' --glob='*.{md,mdx}' .",
"watch": "tsc --noEmit -w",
"test": "yarn generate-static-resources && yarn node --experimental-strip-types --experimental-vm-modules \"$(yarn bin jest)\"",
"remove-version": "node --unhandled-rejections=strict ./scripts/remove-version.js",
Expand Down
2 changes: 1 addition & 1 deletion docs/public/static/data/v55.0.0/expo-maps.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions packages/@expo/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@
### 🐛 Bug fixes

- Add missing `Content-Type: application/json` to internal `/symbolicate` call ([#43074](https://github.com/expo/expo/pull/43074) by [@kitten](https://github.com/kitten))
- Key loader data by `contextKey` instead of URL pathname ([#43017](https://github.com/expo/expo/pull/43017) by [@hassankhan]

### 💡 Others

- Bump to `dnssd-advertise@^1.1.3` ([#42928](https://github.com/expo/expo/pull/42928) by [@kitten](https://github.com/kitten))
- Add more compact QR code rendering for terminals that support it, unless `COLOR=0` is set or TTY is non-interactive ([#42834](https://github.com/expo/expo/pull/42834) by [@kitten](https://github.com/kitten))
- Collapse usage guide on `expo start` if space is insufficient ([#42835](https://github.com/expo/expo/pull/42835) by [@kitten](https://gthub.com/kitten))
- Drop obsolete `rawBody` parsing from Metro middleware stack ([#43074](https://github.com/expo/expo/pull/43074) by [@kitten](https://github.com/kitten))
- Add minimum Node.js version check and warning to CLI startup ([#43076](https://github.com/expo/expo/pull/43076) by [@kitten](https://github.com/kitten))
- Retrieve default route's IP address concurrently ([#42923](https://github.com/expo/expo/pull/42923) by [@kitten](https://github.com/kitten))

## 55.0.7 — 2026-02-08

Expand Down
12 changes: 12 additions & 0 deletions packages/@expo/cli/bin/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ import chalk from 'chalk';
import Debug from 'debug';
import { boolish } from 'getenv';

// Check Node.js version and issue a loud warning if it's too outdated
// This is sent to stderr (console.error) so it doesn't interfere with programmatic commands
const NODE_MIN = [20, 19, 4];
const nodeVersion = process.version?.slice(1).split('.', 3).map(Number);
if (nodeVersion[0] < NODE_MIN[0] || (nodeVersion[0] === NODE_MIN[0] && nodeVersion[1] < NODE_MIN[1])) {
console.error(
chalk.red`{bold Node.js (${process.version}) is outdated and unsupported.}`
+ chalk.red` Please update to a newer Node.js LTS version (required: >=${NODE_MIN.join('.')})\n`
+ chalk.red`Go to: https://nodejs.org/en/download\n`
);
}

// Setup before requiring `debug`.
if (boolish('EXPO_DEBUG', false)) {
Debug.enable('expo:*');
Expand Down
12 changes: 12 additions & 0 deletions packages/@expo/cli/e2e/__tests__/export/server-loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ describe.each(
expect(files).toContain('_expo/loaders/second.js');
expect(files).toContain('_expo/loaders/nullish/[value].js');
expect(files).toContain('_expo/loaders/posts/[postId].js');
expect(files).toContain('_expo/loaders/(group)/index.js');
});

(server.isExpoStart ? it.skip : it)('routes.json has loader paths', async () => {
Expand Down Expand Up @@ -115,6 +116,17 @@ describe.each(
}
);

it.each(getPageAndLoaderData('/(group)'))(
'can access data for group index route $url ($name)',
async ({ getData, url }) => {
const response = await server.fetchAsync(url);
expect(response.status).toBe(200);

const data = await getData(response);
expect(data).toEqual({ data: 'grouped-index' });
}
);

it.each(getPageAndLoaderData('/second'))(
'can access data for $url ($name)',
async ({ getData, url }) => {
Expand Down
16 changes: 14 additions & 2 deletions packages/@expo/cli/e2e/__tests__/export/static-loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,14 @@ describe.each(
expect(files).toContain('_expo/loaders/request');
expect(files).toContain('_expo/loaders/response');
expect(files).toContain('_expo/loaders/second');
expect(files).toContain('_expo/loaders/nested');
expect(files).toContain('_expo/loaders/nested/index');
expect(files).toContain('_expo/loaders/nullish/[value]');
expect(files).toContain('_expo/loaders/nullish/null');
expect(files).toContain('_expo/loaders/nullish/undefined');
expect(files).toContain('_expo/loaders/posts/[postId]');
expect(files).toContain('_expo/loaders/posts/static-post-1');
expect(files).toContain('_expo/loaders/posts/static-post-2');
expect(files).toContain('_expo/loaders/(group)/index');
});

it('returns 404 for loader endpoint when route has no loader', async () => {
Expand Down Expand Up @@ -99,6 +100,17 @@ describe.each(
}
);

it.each(getPageAndLoaderData('/(group)', true))(
'can access data for group index route $url ($name)',
async ({ getData, url }) => {
const response = await server.fetchAsync(url);
expect(response.status).toBe(200);

const data = await getData(response);
expect(data).toEqual({ data: 'grouped-index' });
}
);

it.each(getPageAndLoaderData('/second'))(
'can access data for $url ($name)',
async ({ getData, url }) => {
Expand All @@ -110,7 +122,7 @@ describe.each(
}
);

it.each(getPageAndLoaderData('/nested'))(
it.each(getPageAndLoaderData('/nested', true))(
'can access data for nested index route $url ($name)',
async ({ getData, url }) => {
const response = await server.fetchAsync(url);
Expand Down
8 changes: 6 additions & 2 deletions packages/@expo/cli/e2e/__tests__/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,11 +300,15 @@ export function stripWhitespace(str: string): string {
* @remarks We retrieve the loader first to check for a module ID collision between the main and
* loader bundles. See https://github.com/expo/expo/pull/42245
*/
export function getPageAndLoaderData(url: string) {
export function getPageAndLoaderData(url: string, addIndexSuffixToLoaderPath?: boolean) {
let effectiveLoaderPath = url === '/' ? '/index' : url;
if (addIndexSuffixToLoaderPath) {
effectiveLoaderPath += '/index';
}
return [
{
name: 'loader endpoint',
url: `/_expo/loaders${url === '/' ? '/index' : url}`,
url: `/_expo/loaders${effectiveLoaderPath}`,
getData: (response: Response) => {
return response.json();
},
Expand Down
Loading
Loading