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
@@ -1,5 +1,7 @@
// Copyright © 2018 650 Industries. All rights reserved.

@class RCTBridge;

#import <ExpoModulesCore/EXReactNativeAdapter.h>

@interface EXScopedReactNativeAdapter : EXReactNativeAdapter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ + (NSString *)textForPermissionType:(NSString *)type

- (BOOL)shouldVerifyScopedPermission:(NSString *)permissionType
{
// temporarily exclude notifactions from permissions per experience; system brightness is always granted
// exclude notifications from permissions per experience; system brightness is always granted
return ![@[@"notifications", @"userFacingNotifications", @"systemBrightness"] containsObject:permissionType];
}

Expand Down
2 changes: 2 additions & 0 deletions docs/pages/guides/publishing-websites.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ Run the universal export command to compile the project for web:

The resulting project files are located in the **dist** directory. Any files inside the **public** directory are also copied to the **dist** directory.

> **warning** Avoid creating the directory `/public/assets/`. The path `/assets` is reserved by Metro and will cause file access errors during development.

## Serve locally

Use `npx expo serve` to quickly test locally how your website will be hosted in production. Run the following command to serve the static bundle:
Expand Down
2 changes: 2 additions & 0 deletions docs/pages/router/basics/notation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ Routes that include a `+` have special significance to Expo Router, and are used
- [`+native-intent`](/router/advanced/native-intent/) is used to handle deep links into your app that don't match a specific route, such as links generated by third-party services.
- [`+middleware`](/router/web/middleware/) is used to run code before a route is rendered, allowing you to perform tasks like authentication or redirection for every request.

> **warning** Avoid creating a top-level route named `assets` (like `/app/assets.tsx` or `/app/assets/[other-pages].tsx`). The path `/assets` is reserved by Metro and will cause errors when trying to access it from the URL.

## Route notation applied

Consider the following project file structure to identify the different types of routes represented:
Expand Down
4 changes: 4 additions & 0 deletions docs/pages/router/web/static-rendering.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ export async function generateStaticParams(params: {

</PaddedAPIBox>

> **warning** Avoid creating a top-level route named `assets` (like `/app/assets.tsx` or `/app/assets/[other-pages].tsx`). The path `/assets` is reserved by Metro and will cause errors when trying to access it from the URL.

### Read files using `process.cwd()`

Since Expo Router compiles your code into a separate directory you cannot use `__dirname` to form a path as its value will be different than expected.
Expand Down Expand Up @@ -227,6 +229,8 @@ Expo CLI supports a root **public** directory that gets copied to the **dist** d
files={['public/favicon.ico', 'public/logo.png', 'public/.well-known/apple-app-site-association']}
/>

> **warning** Avoid creating the directory `/public/assets/`. The path `/assets` is reserved by Metro and will cause file access errors during development.

These files will be copied to the **dist** directory during static rendering:

<FileTree
Expand Down
1 change: 1 addition & 0 deletions packages/@expo/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
### 💡 Others

- Replace `minimatch` with `picomatch` and update ([#43323](https://github.com/expo/expo/pull/43323) by [@kitten](https://github.com/kitten))
- Expand logging events ([#43247](https://github.com/expo/expo/pull/43247) by [@kitten](https://github.com/kitten))

## 55.0.10 — 2026-02-20

Expand Down
2 changes: 2 additions & 0 deletions packages/@expo/cli/e2e/__tests__/run-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ afterAll(() => {
it('loads expected modules by default', async () => {
const modules = await getLoadedModulesAsync(`require('../../build/src/run').expoRun`);
expect(modules).toStrictEqual([
'@expo/cli/build/src/events/index.js',
'@expo/cli/build/src/events/stream.js',
'@expo/cli/build/src/log.js',
'@expo/cli/build/src/run/hints.js',
'@expo/cli/build/src/run/index.js',
Expand Down
3 changes: 3 additions & 0 deletions packages/@expo/cli/src/events/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ interface EventLoggerType<Category, Events> {

export interface EventLogger<Category, Events> extends EventLoggerType<Category, Events> {
<EventName extends keyof Events>(event: EventName, data: Events[EventName]): void;

path(target: string): string;
path(target: string | null | undefined): string | null;
}

export interface EventBuilder {
Expand Down
13 changes: 13 additions & 0 deletions packages/@expo/cli/src/events/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface InitMetadata {
version: string;
}

let logPath = process.cwd();
let logStream: LogStream | undefined;

function parseLogTarget(env: string | undefined) {
Expand All @@ -23,6 +24,7 @@ function parseLogTarget(env: string | undefined) {
try {
const parsedPath = path.parse(env);
logDestination = path.format(parsedPath);
logPath = parsedPath.dir;
} catch {
logDestination = undefined;
}
Expand Down Expand Up @@ -110,6 +112,17 @@ export const events: EventLoggerBuilder = ((
}
}
log.category = category;

log.path = function relativePath(target: string | undefined | null): string | null {
try {
return target != null && path.isAbsolute(target)
? path.relative(logPath, target).replace(/\\/, '/') || '.'
: (target ?? null);
} catch {
return target || null;
}
};

return log;
}) as EventLoggerBuilder;

Expand Down
13 changes: 12 additions & 1 deletion packages/@expo/cli/src/events/types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import type { rootEvent } from './index';
import type { collectEventLoggers } from '../events/builder';
import type { event as metroBundlerDevServerEvent } from '../start/server/metro/MetroBundlerDevServer';
import type { event as metroTerminalReporterEvent } from '../start/server/metro/MetroTerminalReporter';
import type { event as instantiateMetroEvent } from '../start/server/metro/instantiateMetro';
import type { event as nodeEnvEvent } from '../utils/nodeEnv';

/** Collection of all event logger events
* @privateRemarks
* When creating a new logger with `events()`, import it here and
* add it to add its types to this union type.
*/
export type Events = collectEventLoggers<[typeof rootEvent, typeof metroTerminalReporterEvent]>;
export type Events = collectEventLoggers<
[
typeof rootEvent,
typeof metroBundlerDevServerEvent,
typeof metroTerminalReporterEvent,
typeof instantiateMetroEvent,
typeof nodeEnvEvent,
]
>;
12 changes: 11 additions & 1 deletion packages/@expo/cli/src/start/server/DevToolsPluginManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,17 @@ export default class DevToolsPluginManager {
).filter((maybePlugin) => maybePlugin != null);
debug('Found autolinked plugins', plugins);
return plugins
.map((pluginInfo) => new DevToolsPlugin(pluginInfo, this.projectRoot))
.map((pluginInfo) => {
try {
return new DevToolsPlugin(pluginInfo, this.projectRoot);
} catch (error: any) {
Log.warn(
`Skipping plugin "${pluginInfo.packageName}": ${error.message ?? 'invalid configuration'}`
);
debug('Plugin validation error for %s: %O', pluginInfo.packageName, error);
return null;
}
})
.filter((p) => p != null) as DevToolsPlugin[];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { Log } from '../../../log';
import DevToolsPluginManager from '../DevToolsPluginManager';

jest.mock('../../../log');

// Mock the autolinking module
jest.mock('expo/internal/unstable-autolinking-exports', () => ({
makeCachedDependenciesLinker: jest.fn(),
scanExpoModuleResolutionsForPlatform: jest.fn(),
getLinkingImplementationForPlatform: jest.fn(),
}));

const autolinking = require('expo/internal/unstable-autolinking-exports') as jest.Mocked<
typeof import('expo-modules-autolinking/exports')
>;

function mockAutolinkingPlugins(
plugins: { packageName: string; packageRoot: string; cliExtensions?: any; webpageRoot?: string }[]
) {
const revisions: Record<string, { name: string }> = {};
const descriptors: Record<string, any> = {};

for (const plugin of plugins) {
revisions[plugin.packageName] = { name: plugin.packageName };
descriptors[plugin.packageName] = plugin;
}

autolinking.makeCachedDependenciesLinker.mockReturnValue({} as any);
autolinking.scanExpoModuleResolutionsForPlatform.mockResolvedValue(revisions as any);
autolinking.getLinkingImplementationForPlatform.mockReturnValue({
resolveModuleAsync: jest.fn(async (name: string) => descriptors[name] ?? null),
} as any);
}

describe('DevToolsPluginManager', () => {
it('should return valid plugins', async () => {
mockAutolinkingPlugins([
{
packageName: 'valid-plugin',
packageRoot: '/path/to/valid-plugin',
webpageRoot: '/web',
},
]);

const manager = new DevToolsPluginManager('/project');
const plugins = await manager.queryPluginsAsync();

expect(plugins.length).toBe(1);
expect(plugins[0].packageName).toBe('valid-plugin');
});

it('should skip a plugin with an invalid config without affecting other valid plugins', async () => {
mockAutolinkingPlugins([
{
packageName: 'valid-plugin',
packageRoot: '/path/to/valid-plugin',
webpageRoot: '/web',
},
{
packageName: 'invalid-plugin',
packageRoot: '/path/to/invalid-plugin',
cliExtensions: {
// Missing required `commands` and `entryPoint` fields
description: 'An invalid extension',
},
},
{
packageName: 'another-valid-plugin',
packageRoot: '/path/to/another-valid-plugin',
cliExtensions: {
description: 'A valid CLI extension',
entryPoint: 'index.js',
commands: [
{
name: 'test-cmd',
title: 'Test Command',
environments: ['cli'],
},
],
},
},
]);

const manager = new DevToolsPluginManager('/project');
const plugins = await manager.queryPluginsAsync();

expect(plugins.length).toBe(2);
expect(plugins[0].packageName).toBe('valid-plugin');
expect(plugins[1].packageName).toBe('another-valid-plugin');
expect(Log.warn).toHaveBeenCalledWith(
expect.stringContaining('Skipping plugin "invalid-plugin"')
);
});
});
30 changes: 30 additions & 0 deletions packages/@expo/cli/src/start/server/metro/MetroBundlerDevServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import {
} from './router';
import { serializeHtmlWithAssets } from './serializeHtml';
import { observeAnyFileChanges, observeFileChanges } from './waitForMetroToObserveTypeScriptFile';
import { events } from '../../../events';
import type {
BundleAssetWithFileHashes,
ExportAssetDescriptor,
Expand Down Expand Up @@ -153,6 +154,22 @@ const EXPO_GO_METRO_PORT = 8081;
/** Default port to use for apps that run in standard React Native projects or Expo Dev Clients. */
const DEV_CLIENT_METRO_PORT = 8081;

// prettier-ignore
export const event = events('devserver', (t) => [
t.event<'start', {
mode: 'production' | 'development';
web: boolean;
baseUrl: string;
asyncRoutes: boolean;
routerRoot: string;
serverComponents: boolean;
serverActions: boolean;
serverRendering: boolean;
apiRoutes: boolean;
exporting: boolean;
}>(),
]);

export class MetroBundlerDevServer extends BundlerDevServer {
private metro: MetroServer | null = null;
private hmrServer: MetroHmrServer<MetroHmrClient> | null = null;
Expand Down Expand Up @@ -1218,6 +1235,19 @@ export class MetroBundlerDevServer extends BundlerDevServer {
// Required for symbolication:
process.env.EXPO_DEV_SERVER_ORIGIN = `http://localhost:${options.port}`;

event('start', {
mode,
web: this.isTargetingWeb(),
baseUrl,
asyncRoutes,
routerRoot: event.path(appDir),
serverComponents: this.isReactServerComponentsEnabled,
serverActions: isReactServerActionsOnlyEnabled,
serverRendering: useServerRendering,
apiRoutes: hasApiRoutes,
exporting: !!options.isExporting,
});

const { metro, hmrServer, server, middleware, messageSocket } = await instantiateMetroAsync(
this,
parsedOptions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,9 @@ export class MetroTerminalReporter extends TerminalReporter {

_logInitializing(port: number, hasReducedPerformance: boolean): void {
// Don't print a giant logo...
this.terminal.log(chalk.dim('Starting Metro Bundler') + '\n');
if (!shouldReduceLogs()) {
this.terminal.log(chalk.dim('Starting Metro Bundler') + '\n');
}
}

shouldFilterClientLog(event: { type: 'client_log'; data: unknown[] }): boolean {
Expand Down
Loading
Loading