diff --git a/apps/notification-tester/app.config.ts b/apps/notification-tester/app.config.ts deleted file mode 100644 index 8110e637c0d5f9..00000000000000 --- a/apps/notification-tester/app.config.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { ExpoConfig, ConfigContext } from '@expo/config'; - -const googleServicesFile = getGoogleServices(); - -export default ({ config }: ConfigContext): ExpoConfig => { - return { - ...config, - name: 'notification-tester', - slug: 'notification-tester', - android: { - ...config.android, - googleServicesFile, - }, - }; -}; - -function getGoogleServices() { - if (process.env.RELEASE) { - return process.env.MICROFOAM_GOOGLE_SERVICES_JSON; - } else if (process.env.PREVIEW) { - return process.env.GOOGLE_SERVICES_JSON_PREVIEW; - } else { - return process.env.GOOGLE_SERVICES_JSON_DEV; - } -} diff --git a/docs/pages/build/introduction.mdx b/docs/pages/build/introduction.mdx index 09bd681c41b879..798af6247aa420 100644 --- a/docs/pages/build/introduction.mdx +++ b/docs/pages/build/introduction.mdx @@ -19,6 +19,8 @@ EAS Build is also designed to work for any native project, whether or not you us ## Quick start +> **info** The `eas` commands below require EAS CLI. See [How to install EAS CLI](/eas/cli/#installation) for more information. + To build your app, run the following command: diff --git a/docs/pages/debugging/runtime-issues.mdx b/docs/pages/debugging/runtime-issues.mdx index f75d41bea12fa1..4e266a91dcad80 100644 --- a/docs/pages/debugging/runtime-issues.mdx +++ b/docs/pages/debugging/runtime-issues.mdx @@ -17,6 +17,8 @@ Whether you're developing your app locally, sending it out to select beta tester Let's go through recommended practices when dealing with each of the above situations. +> **info** Already familiar with React Native debugging? See [Debugging tools](/debugging/tools/) for Expo-specific tooling like React Native DevTools and the built-in profiler. + ## Development errors They are common errors that you encounter while developing your app. Delving into them isn't always straightforward. Usually, debugging when running your app with [Expo CLI](/more/expo-cli/) is enough. @@ -99,42 +101,29 @@ You can now utilize [**Low-level debugger (LLDB)**](https://developer.apple.com/ -> You can delete the **ios** directory when you are done with this process. This ensures that your project remains managed by Expo CLI. Keeping the directory around and manually modifying it outside of `npx expo prebuild` means you'll need to manually upgrade and configure native libraries yourself. - -## Production errors - -Errors or bugs in your production app can be much harder to solve, mainly because you have less context around the error (that is, where, how, and why did the error occur?). +> You can delete or gitignore the **ios** directory when you are done with this process. This ensures that your project remains managed by Expo CLI. Keeping the directory around and manually modifying it outside of `npx expo prebuild` means you'll need to manually upgrade and configure native libraries yourself. -**The best first step in addressing a production error is to reproduce it locally.** Once you reproduce an error locally, you can follow the [development debugging process](#development-errors) to isolate and address the root cause. +## Viewing native logs -> **info** **Tip**: Sometimes, running your app in **production mode** locally will show errors that normally wouldn't be thrown. You can run the app locally in production by running `npx expo start --no-dev --minify`. -> `--no-dev` tells the server not to be run in development mode, and `--minify` is used to minify your code the same way it is for production JavaScript bundles. - -### Production app is crashing - -It can be a frustrating scenario when a production app crashes. There is very little information to look into when it happens. It's important to reproduce the issue, and even if you can't do that, to find any related crash reports. - -Start by reproducing the crash using your production app and then **find an associated crash report**. For Android, you can use `adb logcat` and for iOS you can use the Console app in Xcode. +When your app crashes or behaves unexpectedly, the JavaScript error output doesn't always tell the full story. Native logs from Android and iOS can reveal crash reasons, native module errors, and system-level warnings that don't surface in the Metro bundler or React Native DevTools. -#### Crash reports using adb logcat +### Android: adb logcat -If your Android app is on Google Play, refer to the crashes section of the [Google Play Console](https://play.google.com/console/about/), or connect your Android device to your computer and run the following command: +Connect your Android device (or use an emulator) and run the following command: -The Android Debug Bridge (`adb`) program is part of the Android SDK and allows you to view streaming logs. An alternative to avoid installing Android SDK is to use [WebADB](https://webadb.com/) in Chrome. - -#### Crash reports using Console app +The Android Debug Bridge (`adb`) program is part of the Android SDK and allows you to view streaming logs. An alternative to avoid installing the Android SDK is to use [WebADB](https://webadb.com/) in Chrome. -If your iOS app is on TestFlight or the App Store, you can use the [Crashes Organizer](https://developer.apple.com/news/?id=nra79npr) in Xcode. +### iOS: Console app -If not, you can use the **Console** app in Xcode by connecting your device to your Mac. Follow the steps below on how to access the Console app: +You can use the **Console** app in Xcode by connecting your device to your Mac (or while running an iOS Simulator). Follow the steps below to access the Console app: @@ -158,7 +147,7 @@ If you have connected a physical device, select it under **Devices**. Otherwise, Click on **Open Console** button shown in the window to open the console app. @@ -166,7 +155,22 @@ This will open the console app for you to view logs from your device or simulato -For more information, see Apple's [Diagnosing Issues Using Crash Reports and Device Logs](https://developer.apple.com/documentation/xcode/diagnosing-issues-using-crash-reports-and-device-logs) guide. +## Production errors + +Errors or bugs in your production app can be much harder to solve, mainly because you have less context around the error (that is, where, how, and why did the error occur?). + +**The best first step in addressing a production error is to reproduce it locally.** Once you reproduce an error locally, you can follow the [development debugging process](#development-errors) to isolate and address the root cause. + +### Production app is crashing + +When a production app crashes, there is very little information available compared to development. Start by trying to reproduce the crash locally, then work through these steps to narrow down the cause: + +- **Check platform-specific crash reports.** + - For Android apps on Google Play Store, refer to the crashes section of the [Google Play Console](https://play.google.com/console/about/). + - For iOS apps on TestFlight or the App Store, use the [Crashes Organizer](https://developer.apple.com/news/?id=nra79npr) in Xcode. See also Apple's [Diagnosing Issues Using Crash Reports and Device Logs](https://developer.apple.com/documentation/xcode/diagnosing-issues-using-crash-reports-and-device-logs) guide. +- **Use native log tools.** Connect a device that reproduces the crash and use [`adb logcat` or the Console app](#viewing-native-logs) to capture the native log output. Native logs often reveal the root cause when JavaScript error boundaries don't catch the problem. +- **Try production mode locally.** Running your app in **production mode** locally will show errors that normally wouldn't be thrown. To do so, you can run `npx expo start --no-dev --minify`. The `--no-dev` flag tells the server to run in production mode, and `--minify` is used to minify your code the same way it is for production JavaScript bundles. +- **Check your crash reporting dashboard.** If you use [Sentry](/guides/using-sentry/), [BugSnag](/guides/using-bugsnag/), or a similar service, check for the crash there first. These services provide stack traces, device info, and reproduction context. ### App crashes on certain (older) devices diff --git a/docs/pages/eas-update/introduction.mdx b/docs/pages/eas-update/introduction.mdx index b91912f802e498..467855a5bd4921 100644 --- a/docs/pages/eas-update/introduction.mdx +++ b/docs/pages/eas-update/introduction.mdx @@ -21,9 +21,14 @@ EAS Update makes fixing small bugs and pushing quick fixes a snap in between app ## Quick start +> **info** The `eas` commands below require EAS CLI. See [How to install EAS CLI](/eas/cli/#installation) for more information. + Install the `expo-updates` library and configure EAS Update: - + You need to create a new build for Android or iOS to include the `expo-updates` library in your build. After that, you can push an update to the production channel: diff --git a/docs/pages/eas/hosting/introduction.mdx b/docs/pages/eas/hosting/introduction.mdx index 8299779257b23f..cdc97f0f691363 100644 --- a/docs/pages/eas/hosting/introduction.mdx +++ b/docs/pages/eas/hosting/introduction.mdx @@ -18,6 +18,8 @@ EAS Hosting offers the fastest path from `npx create-expo-app` to a fully deploy ## Quick start +> **info** The `eas` commands below require EAS CLI. See [How to install EAS CLI](/eas/cli/#installation) for more information. + To deploy your web app, you need to create a static build of your web project. Run the following command to export your web project into a **dist** directory: diff --git a/docs/pages/eas/metadata/index.mdx b/docs/pages/eas/metadata/index.mdx index 0d02fd7d166d84..13dcf84bc21fad 100644 --- a/docs/pages/eas/metadata/index.mdx +++ b/docs/pages/eas/metadata/index.mdx @@ -21,6 +21,8 @@ EAS Metadata uses a [**store.config.json**](/eas/metadata/config/#static-store-c ## Quick start +> **info** The `eas` commands below require EAS CLI. See [How to install EAS CLI](/eas/cli/#installation) for more information. + You can push the store config to the app stores by running the following command: diff --git a/docs/pages/eas/workflows/introduction.mdx b/docs/pages/eas/workflows/introduction.mdx index 5aa4a0674bb5ba..b1fbe7e35738ff 100644 --- a/docs/pages/eas/workflows/introduction.mdx +++ b/docs/pages/eas/workflows/introduction.mdx @@ -28,9 +28,11 @@ EAS Workflows run in managed cloud environments with pre-packaged job types desi ## Quick start +> **info** The `eas` commands below require EAS CLI. See [How to install EAS CLI](/eas/cli/#installation) for more information. + Workflows are defined as YAML files in the **.eas/workflows/** directory at the root of your project. Each file specifies a `name`, optional triggers (`on`), and one or more `jobs` that run in the cloud. You can run a workflow with EAS CLI with the following command: - + ## Key features diff --git a/docs/pages/submit/introduction.mdx b/docs/pages/submit/introduction.mdx index bd93f16b287988..45538888e11f9d 100644 --- a/docs/pages/submit/introduction.mdx +++ b/docs/pages/submit/introduction.mdx @@ -21,6 +21,8 @@ EAS Submit works with apps built with [EAS Build](/build/introduction/) or local ## Quick start +> **info** The `eas` commands below require EAS CLI. See [How to install EAS CLI](/eas/cli/#installation) for more information. + Submit an Android build: diff --git a/docs/pages/tutorial/build-a-screen.mdx b/docs/pages/tutorial/build-a-screen.mdx index fae1b511c74000..c3738c2e65b6ba 100644 --- a/docs/pages/tutorial/build-a-screen.mdx +++ b/docs/pages/tutorial/build-a-screen.mdx @@ -7,7 +7,6 @@ hasVideoLink: true import { Collapsible } from '~/ui/components/Collapsible'; import { ContentSpotlight } from '~/ui/components/ContentSpotlight'; import { ProgressTracker } from '~/ui/components/ProgressTracker'; -import { Terminal } from '~/ui/components/Snippet'; import { Step } from '~/ui/components/Step'; import { CODE } from '~/ui/components/Text'; import { VideoBoxLink } from '~/ui/components/VideoBoxLink'; @@ -61,15 +60,7 @@ Now that we've broken down the UI into smaller chunks, we're ready to start codi ## Display the image -We'll use `expo-image` library to display the image in the app. It provides a cross-platform `` component to load and render an image. - -The `expo-image` library comes out of the box, but if you don't have it, stop the development server by pressing Ctrl + c in the terminal. Then, install the `expo-image` library: - - - -The [`npx expo install`](/more/expo-cli/#installation) command will install the library and add it to the project's dependencies in **package.json**. - -> **info** **Tip:** Any time we install a new library in our project, stop the development server by pressing Ctrl + c in the terminal and then run the installation command. After the installation completes, we can start the development server again by running `npx expo start` from the same terminal window. +We'll use `expo-image` library to display the image in the app. It provides a cross-platform `` component to load and render an image. It is already included in the default project template we're using. The Image component takes the source of an image as its value. The source can be either a [static asset](https://reactnative.dev/docs/images#static-image-resources) or a URL. For example, the source required from **assets/images** directory is static. It can also come from [Network](https://reactnative.dev/docs/images#network-images) as a `uri` property. diff --git a/docs/pages/tutorial/image-picker.mdx b/docs/pages/tutorial/image-picker.mdx index 049e6d450574fc..ada3d6f5e4c6ee 100644 --- a/docs/pages/tutorial/image-picker.mdx +++ b/docs/pages/tutorial/image-picker.mdx @@ -28,10 +28,14 @@ We'll use [`expo-image-picker`](/versions/latest/sdk/imagepicker), a library fro ## Install expo-image-picker -To install the library, run the following command: +To install the `expo-image-picker` library, stop the development server by pressing Ctrl + c in the terminal, then run the following command: +The [`npx expo install`](/more/expo-cli/#installation) command will install the library and add it to the project's dependencies in **package.json**. + +> **info** **Tip:** Any time we install a new library in the project, stop the development server by pressing Ctrl + c in the terminal and then run the installation command. After the installation completes, start the development server again by running `npx expo start`. + diff --git a/packages/@expo/cli/CHANGELOG.md b/packages/@expo/cli/CHANGELOG.md index 83240729f34012..dd0d42e95dc878 100644 --- a/packages/@expo/cli/CHANGELOG.md +++ b/packages/@expo/cli/CHANGELOG.md @@ -34,6 +34,7 @@ _This version does not introduce any user-facing changes._ - Retrieve default route's IP address concurrently ([#42923](https://github.com/expo/expo/pull/42923) by [@kitten](https://github.com/kitten)) - Replace `require-from-string` with `@expo/require-utils` ([#42884](https://github.com/expo/expo/pull/42884) by [@kitten](https://github.com/kitten)) - Refactor env loading and reloading to unified logic and don't overwrite original system values ([#43038](https://github.com/expo/expo/pull/43038) by [@kitten](https://github.com/kitten)) +- Add initial event logging system ([#43013](https://github.com/expo/expo/pull/43013) by [@kitten](https://github.com/kitten)) ## 55.0.7 — 2026-02-08 diff --git a/packages/@expo/cli/CLAUDE.md b/packages/@expo/cli/CLAUDE.md index 6f82ff2c6b6443..54e2540f7e99a9 100644 --- a/packages/@expo/cli/CLAUDE.md +++ b/packages/@expo/cli/CLAUDE.md @@ -8,6 +8,7 @@ CLI tool for all Expo projects. The public interface should be lean, all command ├── bin/cli.ts # CLI entry point - registers all commands ├── src/ │ ├── api/ # expo.dev API client +│ ├── events/ # JSONL event-based debugger │ ├── config/ # `expo config` command │ ├── customize/ # `expo customize` command │ ├── export/ # `expo export` command (production bundling) @@ -138,7 +139,42 @@ See `src/utils/open.ts` for an example of handling the `SYSTEMROOT`/`SystemRoot` ## Debug logs -Debug logs support `DEBUG=expo:*`, for legacy reasons we support `EXPO_DEBUG=1` which sets `DEBUG=expo:*` in the bin.ts file. +**Old debugging system:** +Debug logs used to be created with the `debug` package with individual modules creating a `debug` function to use for logging. +This can then be activated with `DEBUG=expo:*`, and for legacy reasons `EXPO_DEBUG=1` currently sets `DEBUG=expo:*` in the bin.ts file. + +```ts +const debug = require('debug')('expo:utils:example'); +debug('hello'); +``` + +**New debugging system:** +Newer modules use the `events` helper from `src/events/index.ts` to define structured events in JSON format. + +```ts +export const event = events('metro', (t) => [ + t.event<'example:start', { + value: string; + }>(), +]); + +event('metro:example:start', { value: 'hello' }); +``` + +The `events` function accepts a category name and a function that is used to define the event types, but never called. +When setting `LOG_EVENTS=1` JSONL events will be logged to the standard output, or with `LOG_EVENTS=events.log` events will log to an events.log file. +This is a faster events system than `debug`, captures structured JSON events, and is scalable, and can be used in any module to add richer debug output. + +When creatin a nwe events category, add the `event` function it returns to the `Events` type in `src/events/types.ts` to collect all the events' types in one place: + +``` +// Add a new import: +import type { event as myNewEvent } from '...'; + +export type Events = collectEventLoggers<[ + typeof myNewEvent, // Add the imported new event function here +]>; +``` ## Production diff --git a/packages/@expo/cli/bin/cli.ts b/packages/@expo/cli/bin/cli.ts index fedda108bb9153..51cf626a9a43e3 100755 --- a/packages/@expo/cli/bin/cli.ts +++ b/packages/@expo/cli/bin/cli.ts @@ -4,6 +4,12 @@ import chalk from 'chalk'; import Debug from 'debug'; import { boolish } from 'getenv'; +import { installEventLogger } from '../src/events'; + +// Setup event logger output +// NOTE: Done before any console output +installEventLogger(); + // 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]; diff --git a/packages/@expo/cli/src/events/__tests__/stream-test.ts b/packages/@expo/cli/src/events/__tests__/stream-test.ts new file mode 100644 index 00000000000000..c3a60353917777 --- /dev/null +++ b/packages/@expo/cli/src/events/__tests__/stream-test.ts @@ -0,0 +1,515 @@ +import fs from 'node:fs'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; + +import { LogStream } from '../stream'; + +jest.unmock('fs'); + +let destFile: string; +let destFD: number; + +beforeEach( + () => + new Promise((resolve, reject) => { + destFile = path.resolve(tmpdir(), `logstream-${Math.random().toString(36).substring(7)}.log`); + fs.open(destFile, 'w', 0o666, (err, fd) => { + if (err) { + reject(err); + } else { + resolve((destFD = fd)); + } + }); + }) +); + +afterEach( + () => + new Promise((resolve) => { + fs.close(destFD, () => { + fs.unlink(destFile, () => { + resolve(); + }); + }); + }) +); + +describe('basic write operations', () => { + it('writes before destroying a stream', async () => { + const stream = new LogStream(destFD); + const _close = new Promise((resolve) => stream.on('close', resolve)); + + expect(stream.write('hello world\n')).toBeTruthy(); + stream.destroy(); + + await _close; + expect(await fs.promises.readFile(destFile, 'utf8')).toBe('hello world\n'); + }); + + it('writes twice then ends a stream', async () => { + const stream = new LogStream(destFD); + const _close = new Promise((resolve) => stream.on('close', resolve)); + + expect(stream.write('line 1\n')).toBeTruthy(); + expect(stream.write('line 2\n')).toBeTruthy(); + stream.end(); + + await _close; + expect(await fs.promises.readFile(destFile, 'utf8')).toBe('line 1\nline 2\n'); + }); + + it('performs partial writes atomically', async () => { + const stream = new LogStream(destFD); + const _close = new Promise((resolve) => stream.on('close', resolve)); + + expect(stream.write('line')).toBeTruthy(); + expect(stream.write(' 1\n')).toBeTruthy(); + stream.destroy(); + + await _close; + expect(await fs.promises.readFile(destFile, 'utf8')).toBe('line 1\n'); + }); + + it('writes multiple lines in a single call', async () => { + const stream = new LogStream(destFD); + const _close = new Promise((resolve) => stream.on('close', resolve)); + + stream.write('line 1\nline 2\nline 3\n'); + stream.end(); + + await _close; + expect(await fs.promises.readFile(destFile, 'utf8')).toBe('line 1\nline 2\nline 3\n'); + }); + + it('handles empty writes', async () => { + const stream = new LogStream(destFD); + const _close = new Promise((resolve) => stream.on('close', resolve)); + + stream.write(''); + stream.write('hello\n'); + stream.write(''); + stream.end(); + + await _close; + expect(await fs.promises.readFile(destFile, 'utf8')).toBe('hello\n'); + }); + + it('writes with Uint8Array input', async () => { + const stream = new LogStream(destFD); + const _close = new Promise((resolve) => stream.on('close', resolve)); + + const data = new TextEncoder().encode('hello world\n'); + stream.write(data); + stream.end(); + + await _close; + expect(await fs.promises.readFile(destFile, 'utf8')).toBe('hello world\n'); + }); +}); + +describe('partial line handling', () => { + it('accumulates multiple partial writes before newline', async () => { + const stream = new LogStream(destFD); + const _close = new Promise((resolve) => stream.on('close', resolve)); + + stream.write('a'); + stream.write('b'); + stream.write('c'); + stream.write('\n'); + stream.end(); + + await _close; + expect(await fs.promises.readFile(destFile, 'utf8')).toBe('abc\n'); + }); + + it('handles interleaved complete and partial lines', async () => { + const stream = new LogStream(destFD); + const _close = new Promise((resolve) => stream.on('close', resolve)); + + stream.write('complete 1\npartial'); + stream.write(' complete 2\n'); + stream.end(); + + await _close; + expect(await fs.promises.readFile(destFile, 'utf8')).toBe('complete 1\npartial complete 2\n'); + }); + + it('does not write incomplete lines on destroy', async () => { + const stream = new LogStream(destFD); + const _close = new Promise((resolve) => stream.on('close', resolve)); + + stream.write('complete\n'); + stream.write('incomplete'); + stream.destroy(); + + await _close; + expect(await fs.promises.readFile(destFile, 'utf8')).toBe('complete\n'); + }); + + it('does not write incomplete lines on end', async () => { + const stream = new LogStream(destFD); + const _close = new Promise((resolve) => stream.on('close', resolve)); + + stream.write('complete\n'); + stream.write('incomplete'); + stream.end(); + + await _close; + expect(await fs.promises.readFile(destFile, 'utf8')).toBe('complete\n'); + }); +}); + +describe('flush behavior', () => { + it('flushes complete lines and calls callback', async () => { + const stream = new LogStream(destFD); + const _close = new Promise((resolve) => stream.on('close', resolve)); + + stream.write('line 1\n'); + + await new Promise((resolve, reject) => { + stream.flush((err) => { + if (err) reject(err); + else resolve(); + }); + }); + + expect(await fs.promises.readFile(destFile, 'utf8')).toBe('line 1\n'); + + stream.end(); + await _close; + }); + + it('preserves partial line data after flush', async () => { + const stream = new LogStream(destFD); + const _close = new Promise((resolve) => stream.on('close', resolve)); + + stream.write('partial'); + + await new Promise((resolve, reject) => { + stream.flush((err) => { + if (err) reject(err); + else resolve(); + }); + }); + + stream.write(' complete\n'); + stream.end(); + + await _close; + expect(await fs.promises.readFile(destFile, 'utf8')).toBe('partial complete\n'); + }); + + it('preserves partial line when flushing with complete lines', async () => { + const stream = new LogStream(destFD); + const _close = new Promise((resolve) => stream.on('close', resolve)); + + stream.write('complete 1\n'); + stream.write('partial'); + + await new Promise((resolve, reject) => { + stream.flush((err) => { + if (err) reject(err); + else resolve(); + }); + }); + + expect(await fs.promises.readFile(destFile, 'utf8')).toBe('complete 1\n'); + + stream.write(' complete 2\n'); + stream.end(); + + await _close; + expect(await fs.promises.readFile(destFile, 'utf8')).toBe('complete 1\npartial complete 2\n'); + }); + + it('handles multiple flushes', async () => { + const stream = new LogStream(destFD); + const _close = new Promise((resolve) => stream.on('close', resolve)); + + stream.write('line 1\n'); + await new Promise((resolve) => stream.flush(() => resolve())); + + stream.write('line 2\n'); + await new Promise((resolve) => stream.flush(() => resolve())); + + stream.end(); + await _close; + + expect(await fs.promises.readFile(destFile, 'utf8')).toBe('line 1\nline 2\n'); + }); + + it('flush on empty stream completes immediately', async () => { + const stream = new LogStream(destFD); + const _close = new Promise((resolve) => stream.on('close', resolve)); + + await new Promise((resolve, reject) => { + stream.flush((err) => { + if (err) reject(err); + else resolve(); + }); + }); + + stream.end(); + await _close; + }); + + it('flush on destroyed stream calls callback immediately', async () => { + const stream = new LogStream(destFD); + const _close = new Promise((resolve) => stream.on('close', resolve)); + + stream.destroy(); + await _close; + + let callbackCalled = false; + stream.flush(() => { + callbackCalled = true; + }); + + expect(callbackCalled).toBe(true); + }); +}); + +describe('end behavior', () => { + it('end with data writes the data', async () => { + const stream = new LogStream(destFD); + const _close = new Promise((resolve) => stream.on('close', resolve)); + + stream.end('final line\n'); + + await _close; + expect(await fs.promises.readFile(destFile, 'utf8')).toBe('final line\n'); + }); + + it('end with callback calls callback on close', async () => { + const stream = new LogStream(destFD); + + await new Promise((resolve) => { + stream.write('line\n'); + stream.end(() => resolve()); + }); + + expect(await fs.promises.readFile(destFile, 'utf8')).toBe('line\n'); + }); + + it('end with data and callback', async () => { + const stream = new LogStream(destFD); + + await new Promise((resolve) => { + stream.end('line\n', resolve); + }); + + expect(await fs.promises.readFile(destFile, 'utf8')).toBe('line\n'); + }); + + it('emits finish event on end', async () => { + const stream = new LogStream(destFD); + const _finish = new Promise((resolve) => stream.on('finish', resolve)); + const _close = new Promise((resolve) => stream.on('close', resolve)); + + stream.write('line\n'); + stream.end(); + + await _finish; + await _close; + }); + + it('multiple end calls are idempotent', async () => { + const stream = new LogStream(destFD); + const _close = new Promise((resolve) => stream.on('close', resolve)); + + stream.write('line\n'); + stream.end(); + stream.end(); + stream.end(); + + await _close; + expect(await fs.promises.readFile(destFile, 'utf8')).toBe('line\n'); + }); +}); + +describe('file path support', () => { + it('opens and writes to file path', async () => { + const filePath = path.resolve(tmpdir(), `logstream-path-${Date.now()}.log`); + const stream = new LogStream(filePath); + + const _ready = new Promise((resolve) => stream.on('ready', resolve)); + await _ready; + + expect(stream.file).toBe(filePath); + expect(stream.fd).toBeGreaterThan(0); + + const _close = new Promise((resolve) => stream.on('close', resolve)); + stream.write('hello from path\n'); + stream.end(); + + await _close; + expect(await fs.promises.readFile(filePath, 'utf8')).toBe('hello from path\n'); + + await fs.promises.unlink(filePath); + }); + + it('creates parent directories for file path', async () => { + const filePath = path.resolve(tmpdir(), `logstream-nested-${Date.now()}`, 'subdir', 'test.log'); + const stream = new LogStream(filePath); + + const _ready = new Promise((resolve) => stream.on('ready', resolve)); + await _ready; + + const _close = new Promise((resolve) => stream.on('close', resolve)); + stream.write('nested file\n'); + stream.end(); + + await _close; + expect(await fs.promises.readFile(filePath, 'utf8')).toBe('nested file\n'); + + await fs.promises.rm(path.dirname(path.dirname(filePath)), { recursive: true }); + }); + + it('queues writes before file is opened', async () => { + const filePath = path.resolve(tmpdir(), `logstream-queue-${Date.now()}.log`); + const stream = new LogStream(filePath); + + stream.write('line 1\n'); + stream.write('line 2\n'); + + const _close = new Promise((resolve) => stream.on('close', resolve)); + stream.end(); + + await _close; + expect(await fs.promises.readFile(filePath, 'utf8')).toBe('line 1\nline 2\n'); + + await fs.promises.unlink(filePath); + }); +}); + +describe('stream properties', () => { + it('writable is true initially', () => { + const stream = new LogStream(destFD); + expect(stream.writable).toBe(true); + }); + + it('writable is false after end', async () => { + const stream = new LogStream(destFD); + const _close = new Promise((resolve) => stream.on('close', resolve)); + + stream.end(); + expect(stream.writable).toBe(false); + + await _close; + }); + + it('writable is false after destroy', async () => { + const stream = new LogStream(destFD); + const _close = new Promise((resolve) => stream.on('close', resolve)); + + stream.destroy(); + expect(stream.writable).toBe(false); + + await _close; + }); + + it('fd returns the file descriptor', () => { + const stream = new LogStream(destFD); + expect(stream.fd).toBe(destFD); + }); + + it('file returns null for fd-based stream', () => { + const stream = new LogStream(destFD); + expect(stream.file).toBeNull(); + }); +}); + +describe('events', () => { + it('emits ready event on next tick for fd', async () => { + const stream = new LogStream(destFD); + const _ready = new Promise((resolve) => stream.on('ready', resolve)); + await _ready; + }); + + it('emits drain event when buffer is flushed', async () => { + const stream = new LogStream(destFD); + const _close = new Promise((resolve) => stream.on('close', resolve)); + + stream.write('line\n'); + + const _drain = new Promise((resolve) => stream.on('drain', resolve)); + await _drain; + + stream.end(); + await _close; + }); + + it('emits write event with bytes written', async () => { + const stream = new LogStream(destFD); + const _close = new Promise((resolve) => stream.on('close', resolve)); + + const writes: number[] = []; + stream.on('write', (written) => writes.push(written)); + + stream.write('hello\n'); + stream.end(); + + await _close; + expect(writes.length).toBeGreaterThan(0); + expect(writes.reduce((a, b) => a + b, 0)).toBe(6); + }); + + it('emits close event on destroy', async () => { + const stream = new LogStream(destFD); + const _close = new Promise((resolve) => stream.on('close', resolve)); + + stream.destroy(); + await _close; + }); +}); + +describe('write returns backpressure signal', () => { + it('returns true when under high water mark', async () => { + const stream = new LogStream(destFD); + const _close = new Promise((resolve) => stream.on('close', resolve)); + + const result = stream.write('small\n'); + expect(result).toBe(true); + + stream.end(); + await _close; + }); + + it('returns false when over high water mark', async () => { + const stream = new LogStream(destFD); + const _close = new Promise((resolve) => stream.on('close', resolve)); + + stream.write('small\n'); + const largeLine = 'x'.repeat(20000) + '\n'; + const result = stream.write(largeLine); + expect(result).toBe(false); + + stream.end(); + await _close; + }); +}); + +describe('destroyed stream behavior', () => { + it('write returns false on destroyed stream', async () => { + const stream = new LogStream(destFD); + const _close = new Promise((resolve) => stream.on('close', resolve)); + + stream.destroy(); + await _close; + + expect(stream.write('ignored\n')).toBe(false); + }); +}); + +describe('Symbol.dispose', () => { + it('disposes the stream', async () => { + const stream = new LogStream(destFD); + const _close = new Promise((resolve) => stream.on('close', resolve)); + + stream.write('line\n'); + stream[Symbol.dispose](); + + await _close; + expect(await fs.promises.readFile(destFile, 'utf8')).toBe('line\n'); + }); +}); diff --git a/packages/@expo/cli/src/events/builder.ts b/packages/@expo/cli/src/events/builder.ts new file mode 100644 index 00000000000000..3afb672f692509 --- /dev/null +++ b/packages/@expo/cli/src/events/builder.ts @@ -0,0 +1,78 @@ +/* eslint-disable @typescript-eslint/no-empty-object-type */ + +type obj = T extends { [key: string | number]: any } ? { [K in keyof T]: T[K] } : never; + +export type EmptyPayload = { + [key: string]: never; + _e: never; + _t: never; +}; + +type AbstractPayload = Record & { + _e?: never; + _t?: never; +}; + +export type Event< + Name extends string = string, + Payload extends AbstractPayload = EmptyPayload, +> = obj<{ key: Name } & Payload>; + +export type EventShape = Name & { + __payload: Payload; +}; + +type getEventPayloadOfShape = + Shape extends EventShape + ? Payload extends AbstractPayload + ? { [K in Name]: Payload } + : never + : never; + +type getEventsOfShapesRec = Shapes extends readonly [infer Shape, ...infer Rest] + ? getEventsOfShapesRec> + : obj; + +type reduceEventLoggerEvents = + EventLogger extends EventLoggerType + ? Category extends string + ? { + [K in keyof Events]: K extends string + ? obj<{ key: `${Category}:${K}` } & Events[K]> + : never; + }[keyof Events] + : never + : never; + +type reduceEventLoggersRec = EventLoggers extends readonly [ + infer EventLogger, + ...infer Rest, +] + ? reduceEventLoggersRec> + : obj; + +interface EventLoggerType { + category: Category; + __eventTypes?: () => Events; +} + +export interface EventLogger extends EventLoggerType { + (event: EventName, data: Events[EventName]): void; +} + +export interface EventBuilder { + event(): EventShape< + Name, + Payload + >; +} + +export interface EventLoggerBuilder { + []>( + category: Category, + _fn: (builder: EventBuilder) => Shapes + ): EventLogger>; +} + +export type collectEventLoggers[]]> = + reduceEventLoggersRec; diff --git a/packages/@expo/cli/src/events/index.ts b/packages/@expo/cli/src/events/index.ts new file mode 100644 index 00000000000000..bfbde7dab0b6b5 --- /dev/null +++ b/packages/@expo/cli/src/events/index.ts @@ -0,0 +1,120 @@ +import { Console } from 'node:console'; +import path from 'node:path'; +import { WriteStream } from 'node:tty'; + +import type { EventBuilder, EventLoggerBuilder, EventShape } from './builder'; +import { LogStream } from './stream'; +import { env } from '../utils/env'; + +interface InitMetadata { + format: 'v0-jsonl' | (string & {}); + version: string; +} + +let logStream: LogStream | undefined; + +function parseLogTarget(env: string | undefined) { + let logDestination: string | number | undefined; + if (env) { + const fd = parseInt(env, 10); + if (fd > 0 && Number.isSafeInteger(fd)) { + logDestination = fd; + } else { + try { + const parsedPath = path.parse(env); + logDestination = path.format(parsedPath); + } catch { + logDestination = undefined; + } + } + } + return logDestination; +} + +function getInitMetadata(): InitMetadata { + return { + format: 'v0-jsonl', + // Version is added in the build script. + version: process.env.__EXPO_VERSION ?? 'UNVERSIONED', + }; +} + +/** Activates the event logger based on the input env var + * @param env - The target to write the logs to; defaults to `$LOG_EVENTS` + */ +export function installEventLogger(env = process.env.LOG_EVENTS) { + const eventLogDestination = parseLogTarget(env); + if (eventLogDestination) { + if (eventLogDestination === 1) { + const output = new WriteStream(2); + Object.defineProperty(process, 'stdout', { get: () => output }); + globalThis.console = new Console(output, output); + } else if (eventLogDestination === 2) { + const output = new WriteStream(1); + Object.defineProperty(process, 'stderr', { get: () => output }); + globalThis.console = new Console(output, output); + } + logStream = new LogStream(eventLogDestination); + rootEvent('init', getInitMetadata()); + } +} + +/** Returns whether the event logger is active */ +export const isEventLoggerActive = () => !!logStream?.writable; + +/** Whether logs shown in the terminal should be reduced. + * @remarks + * We indicate that we're in an automated tool (e.g. E2E tests) with `EXPO_UNSTABLE_HEADLESS`. + * If the event logger is activate and we're running in a headless tool, we should reduce + * interactive or noisy logs, in favour of the event logger. + */ +export const shouldReduceLogs = () => !!logStream && env.EXPO_UNSTABLE_HEADLESS; + +/** Used to create an event logger for structured JSONL logs activated with the `LOG_EVENTS` environment variable. + * + * @remarks + * Structured logs are streamed to a JSONL output file or file descriptor, and are meant for automated tooling + * or normal usage to document what happened during a user session. When creating a module that outputs errors, + * events, or captures what the user was doing, create a new event logger category for them and add structured + * log events. + * For example, `../start/server/metro/MetroTerminalReporter` captures most of Metro's logged events. + * Structured JSONL logs don't have a large performance impact, unlike `DEBUG` logs, and are easily parseable + * and filterable, including by wrapper processes. + * + * After adding a new event category, don't forget to add it to `./types.ts` to collect all event shape types + * in one place. + * + * @example + * ```ts + * export const event = events('test', (t) => [ + * t.event<'my_event', { + * myValue: string | null; + * }>(), + * ]); + * + * event('my_event', { myValue: 'test' }); + * ``` + * + * This will log a `{ _e: 'test:my_event', _t: 0, myValue: 'test' }` entry in the event log. + */ +export const events: EventLoggerBuilder = (( + category: string, + _fn: (builder: EventBuilder) => readonly EventShape[] +) => { + function log(event: string, data: any) { + if (logStream) { + const _e = `${category}:${String(event)}`; + const _t = Date.now(); + const payload = JSON.stringify({ _e, _t, ...data }); + logStream._write(payload + '\n'); + } + } + log.category = category; + return log; +}) as EventLoggerBuilder; + +// These are built-in events: We choose an ambiguous name on purpose, +// since we don't assume this implementation will necessarily only be in `@expo/cli` +export const rootEvent = events('root', (t) => [t.event<'init', InitMetadata>()]); + +export type { EventLogger } from './builder'; diff --git a/packages/@expo/cli/src/events/stream.ts b/packages/@expo/cli/src/events/stream.ts new file mode 100644 index 00000000000000..94464d1065e16f --- /dev/null +++ b/packages/@expo/cli/src/events/stream.ts @@ -0,0 +1,315 @@ +import { Buffer } from 'node:buffer'; +import { EventEmitter } from 'node:events'; +import fs from 'node:fs'; +import path from 'node:path'; + +const BUSY_WRITE_TIMEOUT = 100; +const HIGH_WATER_MARK = 16_387; /*16KB*/ + +export class LogStream extends EventEmitter implements NodeJS.WritableStream { + #fd = -1; + #file: string | null = null; + + #writing = false; + #ending = false; + #flushPending = false; + #destroyed = false; + #opening = false; + + #output = ''; + #len = 0; + #lines: string[] = []; + #partialLine = 0; + + constructor(dest: string | number) { + super(); + if (typeof dest === 'number') { + fs.fsyncSync(dest); + this.#fd = dest; + process.nextTick(() => this.emit('ready')); + } else if (typeof dest === 'string') { + this.#openFile(dest); + } + } + + get file(): string | null { + return this.#file; + } + + get fd(): number { + return this.#fd; + } + + get writing(): boolean { + return this.#writing; + } + + get writable(): boolean { + return !this.#destroyed && !this.#ending; + } + + #release(error: NodeJS.ErrnoException | null, written: number) { + if (error) { + if (error.code === 'EAGAIN' || error.code === 'EBUSY') { + setTimeout(() => this.#writeLine(), BUSY_WRITE_TIMEOUT); + } else { + this.#writing = false; + this.emit('error', error); + } + } else { + this.emit('write', written); + + const outputLength = Buffer.byteLength(this.#output); + if (outputLength > written) { + const output = Buffer.from(this.#output).subarray(written).toString(); + this.#len -= this.#output.length - output.length; + this.#output = output; + } else { + this.#len -= this.#output.length; + this.#output = ''; + } + + if (this.#output || this.#lines.length > this.#partialLine) { + this.#writeLine(); + } else if (this.#ending) { + this.#writing = false; + this.#close(); + } else { + this.#writing = false; + this.emit('drain'); + } + } + } + + #openFile(file: string) { + this.#opening = true; + this.#writing = true; + + const onOpened = (error: Error | null, fd?: number | null) => { + if (error) { + this.#writing = false; + this.#opening = false; + this.emit('error', error); + } else { + this.#fd = fd!; + this.#file = file; + this.#opening = false; + this.#writing = false; + this.emit('ready'); + if (this.#destroyed) { + // do nothing when we're already closing the file + } else if ( + (!this.writing && this.#lines.length > this.#partialLine) || + this.#flushPending + ) { + this.#writeLine(); + } + } + }; + + fs.mkdir(path.dirname(file), { recursive: true }, (err) => { + if (err) return onOpened(err); + fs.open(file, 'a', 0o666, onOpened); + }); + } + + #close() { + if (this.#fd === -1) { + this.once('ready', () => this.#close()); + return; + } + + this.#destroyed = true; + this.#partialLine = 0; + this.#lines.length = 0; + + const onClose = (error?: NodeJS.ErrnoException | null) => { + if (error) { + this.emit('error', error); + this.emit('close', error); + } else { + if (this.#ending && !this.#writing) this.emit('finish'); + this.emit('close'); + } + }; + + fsFsync(this.#fd, (error) => { + if (!error && !isStdFd(this.#fd)) { + fs.close(this.#fd, onClose); + } else { + onClose(); // Error intentionally ignored, assume closed + } + }); + } + + #writeLine() { + this.#writing = true; + this.#output ||= this.#lines.length > this.#partialLine ? this.#lines.shift() || '' : ''; + fs.write(this.#fd, this.#output, (err, written) => this.#release(err, written)); + } + + _end() { + if (!this.#destroyed && !this.#ending) { + this.#ending = true; + if (this.#opening) { + this.once('ready', () => this._end()); + } else if (!this.#writing && this.#fd >= 0) { + if (this.#lines.length > this.#partialLine) { + this.#writeLine(); + } else { + this.#close(); + } + } + } + return this; + } + + end(cb?: () => void): this; + end(data: string | Uint8Array, cb?: () => void): this; + end(str: string, encoding?: BufferEncoding, cb?: () => void): this; + + end( + arg1?: Uint8Array | string | (() => void), + arg2?: BufferEncoding | (() => void), + arg3?: () => void + ) { + const maybeCb = arg3 || arg2 || arg1; + const input = typeof arg1 !== 'function' ? arg1 : undefined; + const encoding = typeof arg2 === 'string' ? arg2 : 'utf8'; + const cb = typeof maybeCb === 'function' ? maybeCb : undefined; + if (typeof input === 'string') { + this.write(input, encoding); + } else if (input != null) { + this.write(input); + } + if (cb) this.once('close', cb); + return this._end(); + } + + destroy() { + if (!this.#destroyed) this.#close(); + } + + flush(cb?: (error?: Error | null) => void) { + if (this.#destroyed) { + cb?.(); + } else { + const onDrain = () => { + if (!this.#destroyed) { + fsFsync(this.#fd, (error) => { + this.#flushPending = false; + if (error?.code === 'EBADF') { + cb?.(); // If fd is closed, ignore the error + } else { + cb?.(error); + } + }); + } else { + this.#flushPending = false; + cb?.(); + } + this.off('error', onError); + }; + + const onError = (err: Error) => { + this.#flushPending = false; + this.off('drain', onDrain); + cb?.(err); + }; + + this.#flushPending = true; + this.once('drain', onDrain); + this.once('error', onError); + + if (!this.#writing) { + if (this.#lines.length > this.#partialLine || this.#output) { + // There are complete lines or remaining output to write + this.#writeLine(); + } else { + // Nothing complete to write, emit drain immediately + process.nextTick(() => this.emit('drain')); + } + } + } + } + + _write(data: string): boolean { + if (this.#destroyed) { + return false; + } + + this.#len += data.length; + + let startIdx = 0; + let endIdx = -1; + while ((endIdx = data.indexOf('\n', startIdx)) > -1) { + const line = data.slice(startIdx, endIdx + 1); + if (this.#partialLine > 0) { + this.#lines[this.#lines.length - 1] += line; + } else { + this.#lines.push(line); + } + this.#partialLine = 0; + startIdx = ++endIdx; + } + + if (startIdx < data.length) { + const line = data.slice(startIdx); + if (this.#partialLine > 0) { + this.#lines[this.#lines.length - 1] += line; + } else { + this.#lines.push(data.slice(startIdx)); + } + this.#partialLine = 1; + } + + if (!this.#writing && this.#lines.length > this.#partialLine) { + this.#writeLine(); + } + + return this.#len < HIGH_WATER_MARK; + } + + write(buffer: Uint8Array | string, cb?: (err?: Error | null) => void): boolean; + write(str: string, encoding?: BufferEncoding, cb?: (err?: Error | null) => void): boolean; + + write( + input: Uint8Array | string, + arg2?: BufferEncoding | ((err?: Error | null) => void), + arg3?: (err?: Error | null) => void + ): boolean { + const maybeCb = arg3 || arg2; + const encoding = typeof arg2 === 'string' ? arg2 : 'utf8'; + const data = typeof input === 'string' ? input : Buffer.from(input).toString(encoding); + const cb = typeof maybeCb === 'function' ? maybeCb : undefined; + try { + return this._write(data); + } finally { + cb?.(); + } + } + + [Symbol.dispose]() { + this.destroy(); + } +} + +const isStdFd = (fd: number) => { + switch (fd) { + case 1: + case 2: + case process.stdout.fd: + case process.stderr.fd: + return true; + default: + return false; + } +}; + +const fsFsync = (fd: number, cb: (error?: NodeJS.ErrnoException | null) => void) => { + try { + fs.fsync(fd, cb); + } catch (error: any) { + cb(error); + } +}; diff --git a/packages/@expo/cli/src/events/types.ts b/packages/@expo/cli/src/events/types.ts new file mode 100644 index 00000000000000..224a912bc023a7 --- /dev/null +++ b/packages/@expo/cli/src/events/types.ts @@ -0,0 +1,10 @@ +import type { rootEvent } from './index'; +import type { collectEventLoggers } from '../events/builder'; +import type { event as metroTerminalReporterEvent } from '../start/server/metro/MetroTerminalReporter'; + +/** 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]>; diff --git a/packages/@expo/cli/src/start/server/metro/MetroTerminalReporter.ts b/packages/@expo/cli/src/start/server/metro/MetroTerminalReporter.ts index 3b0af75b0d63ea..aa689ab72a9198 100644 --- a/packages/@expo/cli/src/start/server/metro/MetroTerminalReporter.ts +++ b/packages/@expo/cli/src/start/server/metro/MetroTerminalReporter.ts @@ -20,9 +20,68 @@ import { parseErrorStringToObject, } from '../serverLogLikeMetro'; import { attachImportStackToRootMessage, nearestImportStack } from './metroErrorInterface'; +import { events, shouldReduceLogs } from '../../../events'; +import { stripAnsi } from '../../../utils/ansi'; + +type ClientLogLevel = + | 'trace' + | 'info' + | 'error' + | 'warn' + | 'log' + | 'group' + | 'groupCollapsed' + | 'groupEnd' + | 'debug'; const debug = require('debug')('expo:metro:logger') as typeof console.log; +// prettier-ignore +export const event = events('metro', (t) => [ + t.event<'bundling:started', { + id: string; + platform: null | string; + environment: null | string; + entry: string; + }>(), + t.event<'bundling:done', { + id: string | null; + ms: number | null; + total: number; + }>(), + t.event<'bundling:failed', { + id: string | null; + filename: string | null; + message: string | null; + importStack: string | null; + targetModuleName: string | null; + originModulePath: string | null; + }>(), + t.event<'bundling:progress', { + id: string | null; + progress: number; + current: number; + total: number; + }>(), + t.event<'server_log', { + level: 'info' | 'warn' | 'error' | null; + data: string | unknown[] | null; + }>(), + t.event<'client_log', { + level: ClientLogLevel | null; + data: unknown[] | null; + }>(), + t.event<'hmr_client_error', { + message: string; + }>(), + t.event<'cache_write_error', { + message: string; + }>(), + t.event<'cache_read_error', { + message: string; + }>(), +]); + const MAX_PROGRESS_BAR_CHAR_WIDTH = 16; const DARK_BLOCK_CHAR = '\u2593'; const LIGHT_BLOCK_CHAR = '\u2591'; @@ -31,14 +90,17 @@ const LIGHT_BLOCK_CHAR = '\u2591'; * Also removes the giant Metro logo from the output. */ export class MetroTerminalReporter extends TerminalReporter { + #lastFailedBuildID: string | undefined; + constructor( - public projectRoot: string, + public serverRoot: string, terminal: Terminal ) { super(terminal); } _log(event: TerminalReportableEvent): void { + this.#captureLog(event); switch (event.type) { case 'unstable_server_log': if (typeof event.data?.[0] === 'string') { @@ -70,67 +132,11 @@ export class MetroTerminalReporter extends TerminalReporter { case 'client_log': { if (this.shouldFilterClientLog(event)) { return; - } - const { level } = event; - - if (!level) { + } else if (event.level != null) { + return this.#onClientLog(event); + } else { break; } - - if (level === 'warn' || (level as string) === 'error') { - let hasStack = false; - const parsed = event.data.map((msg) => { - // Quick check to see if an unsymbolicated stack is being logged. - if (msg.includes('.bundle//&platform=')) { - const stack = parseErrorStringToObject(msg); - if (stack) { - hasStack = true; - } - return stack; - } - return msg; - }); - - if (hasStack) { - (async () => { - const symbolicating = parsed.map((p) => { - if (typeof p === 'string') return p; - return maybeSymbolicateAndFormatJSErrorStackLogAsync(this.projectRoot, level, p); - }); - - let usefulStackCount = 0; - const fallbackIndices: number[] = []; - const symbolicated = (await Promise.allSettled(symbolicating)).map((s, index) => { - if (s.status === 'rejected') { - debug('Error formatting stack', parsed[index], s.reason); - return parsed[index]; - } else if (typeof s.value === 'string') { - return s.value; - } else { - if (!s.value.isFallback) { - usefulStackCount++; - } else { - fallbackIndices.push(index); - } - return s.value.stack; - } - }); - - // Using EXPO_DEBUG we can print all stack - const filtered = - usefulStackCount && !env.EXPO_DEBUG - ? symbolicated.filter((_, index) => !fallbackIndices.includes(index)) - : symbolicated; - - logLikeMetro(this.terminal.log.bind(this.terminal), level, null, ...filtered); - })(); - return; - } - } - - // Overwrite the Metro terminal logging so we can improve the warnings, symbolicate stacks, and inject extra info. - logLikeMetro(this.terminal.log.bind(this.terminal), level, null, ...event.data); - return; } } return super._log(event); @@ -150,23 +156,11 @@ export class MetroTerminalReporter extends TerminalReporter { const platform = env || getPlatformTagForBuildDetails(progress.bundleDetails); const inProgress = phase === 'in_progress'; - let localPath: string; - - if ( + const localPath = typeof progress.bundleDetails?.customTransformOptions?.dom === 'string' && progress.bundleDetails.customTransformOptions.dom.includes(path.sep) - ) { - // Because we use a generated entry file for DOM components, we need to adjust the logging path so it - // shows a unique path for each component. - // Here, we take the relative import path and remove all the starting slashes. - localPath = progress.bundleDetails.customTransformOptions.dom.replace(/^(\.?\.[\\/])+/, ''); - } else { - const inputFile = progress.bundleDetails.entryFile; - - localPath = path.isAbsolute(inputFile) - ? path.relative(this.projectRoot, inputFile) - : inputFile; - } + ? progress.bundleDetails.customTransformOptions.dom.replace(/^(\.?\.[\\/])+/, '') + : this.#normalizePath(progress.bundleDetails.entryFile); if (!inProgress) { const status = phase === 'done' ? `Bundled ` : `Bundling failed `; @@ -175,21 +169,30 @@ export class MetroTerminalReporter extends TerminalReporter { const startTime = this._bundleTimers.get(progress.bundleDetails.buildID!); let time: string = ''; + let ms: number | null = null; if (startTime != null) { const elapsed: bigint = this._getElapsedTime(startTime); const micro = Number(elapsed) / 1000; - const converted = Number(elapsed) / 1e6; + ms = Number(elapsed) / 1e6; // If the milliseconds are < 0.5 then it will display as 0, so we display in microseconds. - if (converted <= 0.5) { + if (ms <= 0.5) { const tenthFractionOfMicro = ((micro * 10) / 1000).toFixed(0); // Format as microseconds to nearest tenth time = chalk.cyan.bold(`0.${tenthFractionOfMicro}ms`); } else { - time = chalk.dim(converted.toFixed(0) + 'ms'); + time = chalk.dim(ms.toFixed(0) + 'ms'); } } + if (phase === 'done') { + event('bundling:done', { + id: progress.bundleDetails.buildID ?? null, + total: progress.totalFileCount, + ms, + }); + } + // iOS Bundled 150ms const plural = progress.totalFileCount === 1 ? '' : 's'; return ( @@ -199,8 +202,17 @@ export class MetroTerminalReporter extends TerminalReporter { ); } - const filledBar = Math.floor(progress.ratio * MAX_PROGRESS_BAR_CHAR_WIDTH); + event('bundling:progress', { + id: progress.bundleDetails.buildID ?? null, + progress: progress.ratio, + total: progress.totalFileCount, + current: progress.transformedFileCount, + }); + if (shouldReduceLogs()) { + return ''; + } + const filledBar = Math.floor(progress.ratio * MAX_PROGRESS_BAR_CHAR_WIDTH); const _progress = inProgress ? chalk.green.bgGreen(DARK_BLOCK_CHAR.repeat(filledBar)) + chalk.bgWhite.white(LIGHT_BLOCK_CHAR.repeat(MAX_PROGRESS_BAR_CHAR_WIDTH - filledBar)) + @@ -211,7 +223,6 @@ export class MetroTerminalReporter extends TerminalReporter { .padStart(progress.totalFileCount.toString().length)}/${progress.totalFileCount})` ) : ''; - return ( platform + chalk.reset.dim(`${path.dirname(localPath)}${path.sep}`) + @@ -258,25 +269,167 @@ export class MetroTerminalReporter extends TerminalReporter { } } + /** + * Workaround to link build ids to bundling errors. + * This works because `_logBundleBuildFailed` is called before `_logBundlingError` in synchronous manner. + * https://github.com/facebook/metro/blob/main/packages/metro/src/Server.js#L939-L945 + */ + _logBundleBuildFailed(buildID: string): void { + this.#lastFailedBuildID = buildID; + super._logBundleBuildFailed(buildID); + } + _logBundlingError(error: SnippetError): void { - const moduleResolutionError = formatUsingNodeStandardLibraryError(this.projectRoot, error); + const importStack = nearestImportStack(error); + const moduleResolutionError = formatUsingNodeStandardLibraryError(this.serverRoot, error); + if (moduleResolutionError) { - let message = maybeAppendCodeFrame(moduleResolutionError, error.message); - message += '\n\n' + nearestImportStack(error); - return this.terminal.log(message); + const message = maybeAppendCodeFrame(moduleResolutionError, error.message); + event('bundling:failed', { + id: this.#lastFailedBuildID ?? null, + message: stripAnsi(message) ?? null, + importStack: importStack ?? null, + filename: error.filename ?? null, + targetModuleName: this.#normalizePath(error.targetModuleName), + originModulePath: this.#normalizePath(error.originModulePath), + }); + + return this.terminal.log(importStack ? `${message}\n\n${importStack}` : message); + } else { + event('bundling:failed', { + id: this.#lastFailedBuildID ?? null, + message: stripAnsi(error.message) ?? null, + importStack: importStack ?? null, + filename: error.filename ?? null, + targetModuleName: error.targetModuleName ?? null, + originModulePath: error.originModulePath ?? null, + }); + + attachImportStackToRootMessage(error, importStack); + + // NOTE(@kitten): Metro drops the stack forcefully when it finds a `SyntaxError`. However, + // this is really unhelpful, since it prevents debugging Babel plugins or reporting bugs + // in Babel plugins or a transformer entirely + if (error.snippet == null && error.stack != null && error instanceof SyntaxError) { + error.message = error.stack; + delete error.stack; + } + + return super._logBundlingError(error); + } + } + + #onClientLog(evt: { type: 'client_log'; level?: ClientLogLevel; data: unknown[] }) { + const { level = 'log', data } = evt; + if (level === 'warn' || (level as string) === 'error') { + let hasStack = false; + const parsed = data.map((msg) => { + // Quick check to see if an unsymbolicated stack is being logged. + if (typeof msg === 'string' && msg.includes('.bundle//&platform=')) { + const stack = parseErrorStringToObject(msg); + if (stack) { + hasStack = true; + } + return stack; + } + return msg; + }); + + if (hasStack) { + (async () => { + const symbolicating = parsed.map((p) => { + if (typeof p === 'string') { + return p; + } else if ( + p && + typeof p === 'object' && + 'message' in p && + typeof p.message === 'string' + ) { + return maybeSymbolicateAndFormatJSErrorStackLogAsync( + this.serverRoot, + level, + p as any + ); + } else { + return null; + } + }); + + let usefulStackCount = 0; + const fallbackIndices: number[] = []; + const symbolicated = (await Promise.allSettled(symbolicating)).map((s, index) => { + if (s.status === 'rejected') { + debug('Error formatting stack', parsed[index], s.reason); + return parsed[index]; + } else if (!s.value) { + return parsed[index]; + } else if (typeof s.value === 'string') { + return s.value; + } else { + if (!s.value.isFallback) { + usefulStackCount++; + } else { + fallbackIndices.push(index); + } + return s.value.stack; + } + }); + + // Using EXPO_DEBUG we can print all stack + const filtered = + usefulStackCount && !env.EXPO_DEBUG + ? symbolicated.filter((_, index) => !fallbackIndices.includes(index)) + : symbolicated; + + event('client_log', { level, data: symbolicated }); + logLikeMetro(this.terminal.log.bind(this.terminal), level, null, ...filtered); + })(); + return; + } } - attachImportStackToRootMessage(error); + event('client_log', { level, data }); + // Overwrite the Metro terminal logging so we can improve the warnings, symbolicate stacks, and inject extra info. + logLikeMetro(this.terminal.log.bind(this.terminal), level, null, ...data); + } - // NOTE(@kitten): Metro drops the stack forcefully when it finds a `SyntaxError`. However, - // this is really unhelpful, since it prevents debugging Babel plugins or reporting bugs - // in Babel plugins or a transformer entirely - if (error.snippet == null && error.stack != null && error instanceof SyntaxError) { - error.message = error.stack; - delete error.stack; + #captureLog(evt: TerminalReportableEvent) { + switch (evt.type) { + case 'bundle_build_started': { + const entry = + typeof evt.bundleDetails?.customTransformOptions?.dom === 'string' && + evt.bundleDetails.customTransformOptions.dom.includes(path.sep) + ? evt.bundleDetails.customTransformOptions.dom.replace(/^(\.?\.[\\/])+/, '') + : this.#normalizePath(evt.bundleDetails.entryFile); + return event('bundling:started', { + id: evt.buildID, + platform: evt.bundleDetails.platform ?? null, + environment: evt.bundleDetails.customTransformOptions?.environment ?? null, + entry, + }); + } + case 'unstable_server_log': + return event('server_log', { + level: evt.level ?? null, + data: evt.data ?? null, + }); + case 'client_log': + // Handled separately: see this.#onClientLog + return; + case 'hmr_client_error': + case 'cache_write_error': + case 'cache_read_error': + return event(evt.type, { + message: evt.error.message, + }); } + } - return super._logBundlingError(error); + #normalizePath(dest: T | undefined): T | string { + return dest != null && path.isAbsolute(dest) + ? path.relative(this.serverRoot, dest) + : ((dest || null) as T); } } @@ -288,7 +441,7 @@ export class MetroTerminalReporter extends TerminalReporter { * @returns error message or null if not a module resolution error */ export function formatUsingNodeStandardLibraryError( - projectRoot: string, + serverRoot: string, error: SnippetError ): string | null { if (!error.message) { @@ -298,7 +451,7 @@ export function formatUsingNodeStandardLibraryError( if (!targetModuleName || !originModulePath) { return null; } - const relativePath = path.relative(projectRoot, originModulePath); + const relativePath = path.relative(serverRoot, originModulePath); const DOCS_PAGE_URL = 'https://docs.expo.dev/workflow/using-libraries/#using-third-party-libraries'; diff --git a/packages/@expo/cli/src/start/server/metro/metroErrorInterface.ts b/packages/@expo/cli/src/start/server/metro/metroErrorInterface.ts index f20f7024cf9e11..204913b8ce2186 100644 --- a/packages/@expo/cli/src/start/server/metro/metroErrorInterface.ts +++ b/packages/@expo/cli/src/start/server/metro/metroErrorInterface.ts @@ -401,14 +401,13 @@ export function likelyContainsCodeFrame(message: string | undefined): boolean { * Walks thru the error cause chain and attaches the import stack to the root error message. * Removes the error stack for import and syntax errors. */ -export const attachImportStackToRootMessage = (err: unknown) => { - if (!(err instanceof Error)) return; - +export const attachImportStackToRootMessage = ( + err: unknown, + importStack = nearestImportStack(err) +) => { // Space out build failures. - const nearestImportStackValue = nearestImportStack(err); - if (nearestImportStackValue) { - err.message += '\n\n' + nearestImportStackValue; - + if (err instanceof Error && importStack) { + err.message += '\n\n' + importStack; if (!isDebug) { // When not debugging remove the stack to avoid cluttering the output and confusing users, // the import stack is the guide to fixing the error. diff --git a/packages/@expo/config/CHANGELOG.md b/packages/@expo/config/CHANGELOG.md index 8e273eb1a84f1a..2c5bafae779d86 100644 --- a/packages/@expo/config/CHANGELOG.md +++ b/packages/@expo/config/CHANGELOG.md @@ -10,6 +10,8 @@ ### 💡 Others +- Support loading configs from `app.config.{mts,cts,mjs,cjs}` ([#43243](https://github.com/expo/expo/pull/43242)) + ## 55.0.6 — 2026-02-16 _This version does not introduce any user-facing changes._ diff --git a/packages/@expo/config/build/Config.js b/packages/@expo/config/build/Config.js index c38eb218c55127..8ac176d19901c5 100644 --- a/packages/@expo/config/build/Config.js +++ b/packages/@expo/config/build/Config.js @@ -312,21 +312,29 @@ function getConfigFilePaths(projectRoot) { staticConfigPath: getStaticConfigFilePath(projectRoot) }; } +const DYNAMIC_CONFIG_EXTS = ['.ts', '.mts', '.cts', '.mjs', '.cjs', '.js']; function getDynamicConfigFilePath(projectRoot) { - for (const fileName of ['app.config.ts', 'app.config.js']) { + const fileNames = DYNAMIC_CONFIG_EXTS.map(ext => `app.config${ext}`); + for (const fileName of fileNames) { const configPath = _path().default.join(projectRoot, fileName); - if (_fs().default.existsSync(configPath)) { - return configPath; - } + try { + const stat = _fs().default.statSync(configPath); + if (stat.isFile()) { + return configPath; + } + } catch {} } return null; } function getStaticConfigFilePath(projectRoot) { for (const fileName of ['app.config.json', 'app.json']) { const configPath = _path().default.join(projectRoot, fileName); - if (_fs().default.existsSync(configPath)) { - return configPath; - } + try { + const stat = _fs().default.statSync(configPath); + if (stat.isFile()) { + return configPath; + } + } catch {} } return null; } diff --git a/packages/@expo/config/build/Config.js.map b/packages/@expo/config/build/Config.js.map index 75be6a9fcd61ae..148f7529f90dd9 100644 --- a/packages/@expo/config/build/Config.js.map +++ b/packages/@expo/config/build/Config.js.map @@ -1 +1 @@ -{"version":3,"file":"Config.js","names":["_jsonFile","data","_interopRequireDefault","require","_deepmerge","_fs","_glob","_path","_resolveFrom","_semver","_slugify","_getConfig","_getExpoSDKVersion","_withConfigPlugins","_withInternal","_resolvePackageJson","_Config","Object","keys","forEach","key","prototype","hasOwnProperty","call","_exportNames","exports","defineProperty","enumerable","get","e","__esModule","default","hasWarnedAboutRootConfig","reduceExpoObject","config","expo","filter","length","ansiYellow","str","ansiGray","ansiBold","plural","console","warn","map","join","mods","getSupportedPlatforms","projectRoot","platforms","resolveFrom","silent","push","getConfig","options","paths","getConfigFilePaths","rawStaticConfig","staticConfigPath","getStaticConfig","rootConfig","staticConfig","packageJson","packageJsonPath","getPackageJsonAndPath","fillAndReturnConfig","dynamicConfigObjectType","mayHaveUnusedStaticConfig","configWithDefaultValues","ensureConfigHasDefaultValues","exp","pkg","skipSDKVersionRequirement","dynamicConfigPath","hasUnusedStaticConfig","isModdedConfig","withConfigPlugins","skipPlugins","isPublicConfig","_internal","hooks","ios","android","updates","codeSigningCertificate","codeSigningMetadata","getContextConfig","exportedObjectType","rawDynamicConfig","getDynamicConfig","dynamicConfig","getPackageJson","getRootPackageJsonPath","JsonFile","read","getDynamicConfigFilePath","getStaticConfigFilePath","fileName","configPath","path","fs","existsSync","modifyConfigAsync","modifications","readOptions","writeOptions","isDryRun","dryRun","outputConfig","mergeConfigModifications","writeAsync","json5","type","message","relative","newConfig","newConfighasModifications","isMatchingObject","plugins","modifiedExpoConfig","deepMerge","existingPlugins","fromEntries","definition","undefined","plugin","pluginName","pluginProps","Array","isArray","existingPlugin","existingPluginName","finalizedConfig","expectedValues","actualValues","withInternal","pkgName","name","basename","pkgVersion","version","pkgWithDefaults","slug","slugify","toLowerCase","description","expWithDefaults","sdkVersion","getExpoSDKVersion","error","DEFAULT_BUILD_PATH","getWebOutputPath","process","env","WEBPACK_BUILD_OUTPUT_PATH","web","build","output","getNameFromConfig","appManifest","appName","displayName","webName","getDefaultTarget","semver","lt","isBareWorkflowProject","dependencies","expokit","xcodeprojFiles","globSync","absolute","cwd","gradleFiles","getProjectConfigDescription","getProjectConfigDescriptionWithPaths","projectConfig","relativeDynamicConfigPath"],"sources":["../src/Config.ts"],"sourcesContent":["import { ModConfig } from '@expo/config-plugins';\nimport JsonFile, { JSONObject } from '@expo/json-file';\nimport deepMerge from 'deepmerge';\nimport fs from 'fs';\nimport { sync as globSync } from 'glob';\nimport path from 'path';\nimport resolveFrom from 'resolve-from';\nimport semver from 'semver';\nimport slugify from 'slugify';\n\nimport {\n AppJSONConfig,\n ConfigFilePaths,\n ExpoConfig,\n GetConfigOptions,\n PackageJSONConfig,\n Platform,\n ProjectConfig,\n ProjectTarget,\n WriteConfigOptions,\n} from './Config.types';\nimport { getDynamicConfig, getStaticConfig } from './getConfig';\nimport { getExpoSDKVersion } from './getExpoSDKVersion';\nimport { withConfigPlugins } from './plugins/withConfigPlugins';\nimport { withInternal } from './plugins/withInternal';\nimport { getRootPackageJsonPath } from './resolvePackageJson';\n\ntype SplitConfigs = { expo?: ExpoConfig; mods?: ModConfig };\n\nlet hasWarnedAboutRootConfig = false;\n\n/**\n * If a config has an `expo` object then that will be used as the config.\n * This method reduces out other top level values if an `expo` object exists.\n *\n * @param config Input config object to reduce\n */\nfunction reduceExpoObject(config?: any): SplitConfigs | null {\n if (!config) return config || null;\n\n if (config.expo && !hasWarnedAboutRootConfig) {\n const keys = Object.keys(config).filter((key) => key !== 'expo');\n if (keys.length) {\n hasWarnedAboutRootConfig = true;\n const ansiYellow = (str: string) => `\\u001B[33m${str}\\u001B[0m`;\n const ansiGray = (str: string) => `\\u001B[90m${str}\\u001B[0m`;\n const ansiBold = (str: string) => `\\u001B[1m${str}\\u001B[22m`;\n const plural = keys.length > 1;\n console.warn(\n ansiYellow(\n ansiBold('Warning: ') +\n `Root-level ${ansiBold(`\"expo\"`)} object found. Ignoring extra key${plural ? 's' : ''} in Expo config: ${keys\n .map((key) => `\"${key}\"`)\n .join(', ')}\\n` +\n ansiGray(`Learn more: https://expo.fyi/root-expo-object`)\n )\n );\n }\n }\n\n const { mods, ...expo } = config.expo ?? config;\n\n return {\n expo,\n mods,\n };\n}\n\n/**\n * Get all platforms that a project is currently capable of running.\n *\n * @param projectRoot\n * @param exp\n */\nfunction getSupportedPlatforms(projectRoot: string): Platform[] {\n const platforms: Platform[] = [];\n if (resolveFrom.silent(projectRoot, 'react-native')) {\n platforms.push('ios', 'android');\n }\n if (resolveFrom.silent(projectRoot, 'react-dom')) {\n platforms.push('web');\n }\n return platforms;\n}\n\n/**\n * Evaluate the config for an Expo project.\n * If a function is exported from the `app.config.js` then a partial config will be passed as an argument.\n * The partial config is composed from any existing app.json, and certain fields from the `package.json` like name and description.\n *\n * If options.isPublicConfig is true, the Expo config will include only public-facing options (omitting private keys).\n * The resulting config should be suitable for hosting or embedding in a publicly readable location.\n *\n * **Example**\n * ```js\n * module.exports = function({ config }) {\n * // mutate the config before returning it.\n * config.slug = 'new slug'\n * return { expo: config };\n * }\n * ```\n *\n * **Supports**\n * - `app.config.ts`\n * - `app.config.js`\n * - `app.config.json`\n * - `app.json`\n *\n * @param projectRoot the root folder containing all of your application code\n * @param options enforce criteria for a project config\n */\nexport function getConfig(projectRoot: string, options: GetConfigOptions = {}): ProjectConfig {\n const paths = getConfigFilePaths(projectRoot);\n\n const rawStaticConfig = paths.staticConfigPath ? getStaticConfig(paths.staticConfigPath) : null;\n // For legacy reasons, always return an object.\n const rootConfig = (rawStaticConfig || {}) as AppJSONConfig;\n const staticConfig = reduceExpoObject(rawStaticConfig) || {};\n\n // Can only change the package.json location if an app.json or app.config.json exists\n const [packageJson, packageJsonPath] = getPackageJsonAndPath(projectRoot);\n\n function fillAndReturnConfig(\n config: SplitConfigs,\n dynamicConfigObjectType: string | null,\n mayHaveUnusedStaticConfig: boolean = false\n ) {\n const configWithDefaultValues = {\n ...ensureConfigHasDefaultValues({\n projectRoot,\n exp: config.expo || {},\n pkg: packageJson,\n skipSDKVersionRequirement: options.skipSDKVersionRequirement,\n paths,\n packageJsonPath,\n }),\n mods: config.mods,\n dynamicConfigObjectType,\n rootConfig,\n dynamicConfigPath: paths.dynamicConfigPath,\n staticConfigPath: paths.staticConfigPath,\n hasUnusedStaticConfig:\n !!paths.staticConfigPath && !!paths.dynamicConfigPath && mayHaveUnusedStaticConfig,\n };\n\n if (options.isModdedConfig) {\n // @ts-ignore: Add the mods back to the object.\n configWithDefaultValues.exp.mods = config.mods ?? null;\n }\n\n // Apply static json plugins, should be done after _internal\n configWithDefaultValues.exp = withConfigPlugins(\n configWithDefaultValues.exp,\n !!options.skipPlugins\n );\n\n if (!options.isModdedConfig) {\n // @ts-ignore: Delete mods added by static plugins when they won't have a chance to be evaluated\n delete configWithDefaultValues.exp.mods;\n }\n\n if (options.isPublicConfig) {\n // TODD(EvanBacon): Drop plugins array after it's been resolved.\n\n // Remove internal values with references to user's file paths from the public config.\n delete configWithDefaultValues.exp._internal;\n\n // hooks no longer exists in the typescript type but should still be removed\n if ('hooks' in configWithDefaultValues.exp) {\n delete configWithDefaultValues.exp.hooks;\n }\n if (configWithDefaultValues.exp.ios?.config) {\n delete configWithDefaultValues.exp.ios.config;\n }\n if (configWithDefaultValues.exp.android?.config) {\n delete configWithDefaultValues.exp.android.config;\n }\n\n delete configWithDefaultValues.exp.updates?.codeSigningCertificate;\n delete configWithDefaultValues.exp.updates?.codeSigningMetadata;\n }\n\n return configWithDefaultValues;\n }\n\n // Fill in the static config\n function getContextConfig(config: SplitConfigs) {\n return ensureConfigHasDefaultValues({\n projectRoot,\n exp: config.expo || {},\n pkg: packageJson,\n skipSDKVersionRequirement: true,\n paths,\n packageJsonPath,\n }).exp;\n }\n\n if (paths.dynamicConfigPath) {\n // No app.config.json or app.json but app.config.js\n const {\n exportedObjectType,\n config: rawDynamicConfig,\n mayHaveUnusedStaticConfig,\n } = getDynamicConfig(paths.dynamicConfigPath, {\n projectRoot,\n staticConfigPath: paths.staticConfigPath,\n packageJsonPath,\n config: getContextConfig(staticConfig),\n });\n // Allow for the app.config.js to `export default null;`\n // Use `dynamicConfigPath` to detect if a dynamic config exists.\n const dynamicConfig = reduceExpoObject(rawDynamicConfig) || {};\n return fillAndReturnConfig(dynamicConfig, exportedObjectType, mayHaveUnusedStaticConfig);\n }\n\n // No app.config.js but json or no config\n return fillAndReturnConfig(staticConfig || {}, null);\n}\n\nexport function getPackageJson(projectRoot: string): PackageJSONConfig {\n const [pkg] = getPackageJsonAndPath(projectRoot);\n return pkg;\n}\n\nfunction getPackageJsonAndPath(projectRoot: string): [PackageJSONConfig, string] {\n const packageJsonPath = getRootPackageJsonPath(projectRoot);\n return [JsonFile.read(packageJsonPath), packageJsonPath];\n}\n\n/**\n * Get the static and dynamic config paths for a project. Also accounts for custom paths.\n *\n * @param projectRoot\n */\nexport function getConfigFilePaths(projectRoot: string): ConfigFilePaths {\n return {\n dynamicConfigPath: getDynamicConfigFilePath(projectRoot),\n staticConfigPath: getStaticConfigFilePath(projectRoot),\n };\n}\n\nfunction getDynamicConfigFilePath(projectRoot: string): string | null {\n for (const fileName of ['app.config.ts', 'app.config.js']) {\n const configPath = path.join(projectRoot, fileName);\n if (fs.existsSync(configPath)) {\n return configPath;\n }\n }\n return null;\n}\n\nfunction getStaticConfigFilePath(projectRoot: string): string | null {\n for (const fileName of ['app.config.json', 'app.json']) {\n const configPath = path.join(projectRoot, fileName);\n if (fs.existsSync(configPath)) {\n return configPath;\n }\n }\n return null;\n}\n\n/**\n * Attempt to modify an Expo project config.\n * This will only fully work if the project is using static configs only.\n * Otherwise 'warn' | 'fail' will return with a message about why the config couldn't be updated.\n * The potentially modified config object will be returned for testing purposes.\n *\n * @param projectRoot\n * @param modifications modifications to make to an existing config\n * @param readOptions options for reading the current config file\n * @param writeOptions If true, the static config file will not be rewritten\n */\nexport async function modifyConfigAsync(\n projectRoot: string,\n modifications: Partial,\n readOptions: GetConfigOptions = {},\n writeOptions: WriteConfigOptions = {}\n): Promise<{\n type: 'success' | 'warn' | 'fail';\n message?: string;\n config: ExpoConfig | null;\n}> {\n const config = getConfig(projectRoot, readOptions);\n const isDryRun = writeOptions.dryRun;\n\n // Create or modify the static config, when not using dynamic config\n if (!config.dynamicConfigPath) {\n const outputConfig = mergeConfigModifications(config, modifications);\n\n if (!isDryRun) {\n const configPath = config.staticConfigPath ?? path.join(projectRoot, 'app.json');\n await JsonFile.writeAsync(configPath, outputConfig, { json5: false });\n }\n\n return { type: 'success', config: outputConfig.expo ?? outputConfig };\n }\n\n // Attempt to write to a function-like dynamic config, when used with a static config\n if (\n config.staticConfigPath &&\n config.dynamicConfigObjectType === 'function' &&\n !modifications.hasOwnProperty('plugins') // We don't know what plugins are in dynamic configs\n ) {\n const outputConfig = mergeConfigModifications(config, modifications);\n\n if (isDryRun) {\n return {\n type: 'warn',\n message: `Cannot verify config modifications in dry-run mode for config at: ${path.relative(projectRoot, config.dynamicConfigPath)}`,\n config: null,\n };\n }\n\n // Attempt to write the static config with the config modifications\n await JsonFile.writeAsync(config.staticConfigPath, outputConfig, { json5: false });\n\n // Verify that the dynamic config is using the static config\n const newConfig = getConfig(projectRoot, readOptions);\n const newConfighasModifications = isMatchingObject(modifications, newConfig.exp);\n if (newConfighasModifications) {\n return {\n type: 'success',\n config: newConfig.exp,\n };\n }\n\n // Rollback the changes when the reloaded config did not include the modifications\n await JsonFile.writeAsync(config.staticConfigPath, config.rootConfig, { json5: false });\n }\n\n // We cannot automatically write to a dynamic config\n return {\n type: 'warn',\n message: `Cannot automatically write to dynamic config at: ${path.relative(\n projectRoot,\n config.dynamicConfigPath\n )}`,\n config: null,\n };\n}\n\n/**\n * Merge the config modifications, using an optional possible top-level `expo` object.\n * Note, changes in the plugins are merged differently to avoid duplicate entries.\n */\nfunction mergeConfigModifications(\n config: ProjectConfig,\n { plugins, ...modifications }: Partial\n): AppJSONConfig {\n const modifiedExpoConfig: ExpoConfig = !config.rootConfig.expo\n ? deepMerge(config.rootConfig, modifications)\n : deepMerge(config.rootConfig.expo, modifications);\n\n if (plugins?.length) {\n // When adding plugins, ensure the config has a plugin list\n if (!modifiedExpoConfig.plugins) {\n modifiedExpoConfig.plugins = [];\n }\n\n // Create a plugin lookup map\n const existingPlugins: Record = Object.fromEntries(\n modifiedExpoConfig.plugins.map((definition) =>\n typeof definition === 'string' ? [definition, undefined] : definition\n )\n );\n\n for (const plugin of plugins) {\n // Unpack the plugin definition, using either the short (string) or normal (array) notation\n const [pluginName, pluginProps] = Array.isArray(plugin) ? plugin : [plugin];\n // Abort if the plugin definition is empty\n if (!pluginName) continue;\n\n // Add the plugin if it doesn't exist yet, including its properties\n if (!(pluginName in existingPlugins)) {\n modifiedExpoConfig.plugins.push(plugin);\n continue;\n }\n\n // If the plugin has properties, and it exists, merge the properties\n if (pluginProps) {\n modifiedExpoConfig.plugins = modifiedExpoConfig.plugins.map((existingPlugin) => {\n const [existingPluginName] = Array.isArray(existingPlugin)\n ? existingPlugin\n : [existingPlugin];\n\n // Do not modify other plugins\n if (existingPluginName !== pluginName) {\n return existingPlugin;\n }\n\n // Add the props to the existing plugin entry\n if (typeof existingPlugin === 'string') {\n return [existingPlugin, pluginProps];\n }\n\n // Merge the props to the existing plugin properties\n if (Array.isArray(existingPlugin) && existingPlugin[0]) {\n return [existingPlugin[0], deepMerge(existingPlugin[1] ?? {}, pluginProps)];\n }\n\n return existingPlugin;\n });\n continue;\n }\n\n // If the same plugin exists with properties, and the modification does not contain properties, ignore\n }\n }\n\n const finalizedConfig = !config.rootConfig.expo\n ? modifiedExpoConfig\n : { ...config.rootConfig, expo: modifiedExpoConfig };\n\n return finalizedConfig as AppJSONConfig;\n}\n\nfunction isMatchingObject>(\n expectedValues: T,\n actualValues: T\n): boolean {\n for (const key in expectedValues) {\n if (!expectedValues.hasOwnProperty(key)) {\n continue;\n }\n\n if (typeof expectedValues[key] === 'object' && actualValues[key] !== null) {\n if (!isMatchingObject(expectedValues[key], actualValues[key])) {\n return false;\n }\n } else {\n if (expectedValues[key] !== actualValues[key]) {\n return false;\n }\n }\n }\n return true;\n}\n\nfunction ensureConfigHasDefaultValues({\n projectRoot,\n exp,\n pkg,\n paths,\n packageJsonPath,\n skipSDKVersionRequirement = false,\n}: {\n projectRoot: string;\n exp: Partial | null;\n pkg: JSONObject;\n skipSDKVersionRequirement?: boolean;\n paths?: ConfigFilePaths;\n packageJsonPath?: string;\n}): { exp: ExpoConfig; pkg: PackageJSONConfig } {\n if (!exp) {\n exp = {};\n }\n exp = withInternal(exp as any, {\n projectRoot,\n ...(paths ?? {}),\n packageJsonPath,\n });\n // Defaults for package.json fields\n const pkgName = typeof pkg.name === 'string' ? pkg.name : path.basename(projectRoot);\n const pkgVersion = typeof pkg.version === 'string' ? pkg.version : '1.0.0';\n\n const pkgWithDefaults = { ...pkg, name: pkgName, version: pkgVersion };\n\n // Defaults for app.json/app.config.js fields\n const name = exp.name ?? pkgName;\n const slug = exp.slug ?? slugify(name.toLowerCase());\n const version = exp.version ?? pkgVersion;\n let description = exp.description;\n if (!description && typeof pkg.description === 'string') {\n description = pkg.description;\n }\n\n const expWithDefaults = { ...exp, name, slug, version, description };\n\n let sdkVersion;\n try {\n sdkVersion = getExpoSDKVersion(projectRoot, expWithDefaults);\n } catch (error) {\n if (!skipSDKVersionRequirement) throw error;\n }\n\n let platforms = exp.platforms;\n if (!platforms) {\n platforms = getSupportedPlatforms(projectRoot);\n }\n\n return {\n exp: { ...expWithDefaults, sdkVersion, platforms },\n pkg: pkgWithDefaults,\n };\n}\n\nconst DEFAULT_BUILD_PATH = `web-build`;\n\nexport function getWebOutputPath(config: { [key: string]: any } = {}): string {\n if (process.env.WEBPACK_BUILD_OUTPUT_PATH) {\n return process.env.WEBPACK_BUILD_OUTPUT_PATH;\n }\n const expo = config.expo || config || {};\n return expo?.web?.build?.output || DEFAULT_BUILD_PATH;\n}\n\nexport function getNameFromConfig(exp: Record = {}): {\n appName?: string;\n webName?: string;\n} {\n // For RN CLI support\n const appManifest = exp.expo || exp;\n const { web = {} } = appManifest;\n\n // rn-cli apps use a displayName value as well.\n const appName = exp.displayName || appManifest.displayName || appManifest.name;\n const webName = web.name || appName;\n\n return {\n appName,\n webName,\n };\n}\n\nexport function getDefaultTarget(\n projectRoot: string,\n exp?: Pick\n): ProjectTarget {\n exp ??= getConfig(projectRoot, { skipSDKVersionRequirement: true }).exp;\n\n // before SDK 37, always default to managed to preserve previous behavior\n if (exp.sdkVersion && exp.sdkVersion !== 'UNVERSIONED' && semver.lt(exp.sdkVersion, '37.0.0')) {\n return 'managed';\n }\n return isBareWorkflowProject(projectRoot) ? 'bare' : 'managed';\n}\n\nfunction isBareWorkflowProject(projectRoot: string): boolean {\n const [pkg] = getPackageJsonAndPath(projectRoot);\n\n // TODO: Drop this\n if (pkg.dependencies && pkg.dependencies.expokit) {\n return false;\n }\n\n const xcodeprojFiles = globSync('ios/**/*.xcodeproj', {\n absolute: true,\n cwd: projectRoot,\n });\n if (xcodeprojFiles.length) {\n return true;\n }\n const gradleFiles = globSync('android/**/*.gradle', {\n absolute: true,\n cwd: projectRoot,\n });\n if (gradleFiles.length) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Return a useful name describing the project config.\n * - dynamic: app.config.js\n * - static: app.json\n * - custom path app config relative to root folder\n * - both: app.config.js or app.json\n */\nexport function getProjectConfigDescription(projectRoot: string): string {\n const paths = getConfigFilePaths(projectRoot);\n return getProjectConfigDescriptionWithPaths(projectRoot, paths);\n}\n\n/**\n * Returns a string describing the configurations used for the given project root.\n * Will return null if no config is found.\n *\n * @param projectRoot\n * @param projectConfig\n */\nexport function getProjectConfigDescriptionWithPaths(\n projectRoot: string,\n projectConfig: ConfigFilePaths\n): string {\n if (projectConfig.dynamicConfigPath) {\n const relativeDynamicConfigPath = path.relative(projectRoot, projectConfig.dynamicConfigPath);\n if (projectConfig.staticConfigPath) {\n return `${relativeDynamicConfigPath} or ${path.relative(\n projectRoot,\n projectConfig.staticConfigPath\n )}`;\n }\n return relativeDynamicConfigPath;\n } else if (projectConfig.staticConfigPath) {\n return path.relative(projectRoot, projectConfig.staticConfigPath);\n }\n // If a config doesn't exist, our tooling will generate a static app.json\n return 'app.json';\n}\n\nexport * from './Config.types';\n"],"mappings":"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","ignoreList":[]} \ No newline at end of file +{"version":3,"file":"Config.js","names":["_jsonFile","data","_interopRequireDefault","require","_deepmerge","_fs","_glob","_path","_resolveFrom","_semver","_slugify","_getConfig","_getExpoSDKVersion","_withConfigPlugins","_withInternal","_resolvePackageJson","_Config","Object","keys","forEach","key","prototype","hasOwnProperty","call","_exportNames","exports","defineProperty","enumerable","get","e","__esModule","default","hasWarnedAboutRootConfig","reduceExpoObject","config","expo","filter","length","ansiYellow","str","ansiGray","ansiBold","plural","console","warn","map","join","mods","getSupportedPlatforms","projectRoot","platforms","resolveFrom","silent","push","getConfig","options","paths","getConfigFilePaths","rawStaticConfig","staticConfigPath","getStaticConfig","rootConfig","staticConfig","packageJson","packageJsonPath","getPackageJsonAndPath","fillAndReturnConfig","dynamicConfigObjectType","mayHaveUnusedStaticConfig","configWithDefaultValues","ensureConfigHasDefaultValues","exp","pkg","skipSDKVersionRequirement","dynamicConfigPath","hasUnusedStaticConfig","isModdedConfig","withConfigPlugins","skipPlugins","isPublicConfig","_internal","hooks","ios","android","updates","codeSigningCertificate","codeSigningMetadata","getContextConfig","exportedObjectType","rawDynamicConfig","getDynamicConfig","dynamicConfig","getPackageJson","getRootPackageJsonPath","JsonFile","read","getDynamicConfigFilePath","getStaticConfigFilePath","DYNAMIC_CONFIG_EXTS","fileNames","ext","fileName","configPath","path","stat","fs","statSync","isFile","modifyConfigAsync","modifications","readOptions","writeOptions","isDryRun","dryRun","outputConfig","mergeConfigModifications","writeAsync","json5","type","message","relative","newConfig","newConfighasModifications","isMatchingObject","plugins","modifiedExpoConfig","deepMerge","existingPlugins","fromEntries","definition","undefined","plugin","pluginName","pluginProps","Array","isArray","existingPlugin","existingPluginName","finalizedConfig","expectedValues","actualValues","withInternal","pkgName","name","basename","pkgVersion","version","pkgWithDefaults","slug","slugify","toLowerCase","description","expWithDefaults","sdkVersion","getExpoSDKVersion","error","DEFAULT_BUILD_PATH","getWebOutputPath","process","env","WEBPACK_BUILD_OUTPUT_PATH","web","build","output","getNameFromConfig","appManifest","appName","displayName","webName","getDefaultTarget","semver","lt","isBareWorkflowProject","dependencies","expokit","xcodeprojFiles","globSync","absolute","cwd","gradleFiles","getProjectConfigDescription","getProjectConfigDescriptionWithPaths","projectConfig","relativeDynamicConfigPath"],"sources":["../src/Config.ts"],"sourcesContent":["import { ModConfig } from '@expo/config-plugins';\nimport JsonFile, { JSONObject } from '@expo/json-file';\nimport deepMerge from 'deepmerge';\nimport fs from 'fs';\nimport { sync as globSync } from 'glob';\nimport path from 'path';\nimport resolveFrom from 'resolve-from';\nimport semver from 'semver';\nimport slugify from 'slugify';\n\nimport {\n AppJSONConfig,\n ConfigFilePaths,\n ExpoConfig,\n GetConfigOptions,\n PackageJSONConfig,\n Platform,\n ProjectConfig,\n ProjectTarget,\n WriteConfigOptions,\n} from './Config.types';\nimport { getDynamicConfig, getStaticConfig } from './getConfig';\nimport { getExpoSDKVersion } from './getExpoSDKVersion';\nimport { withConfigPlugins } from './plugins/withConfigPlugins';\nimport { withInternal } from './plugins/withInternal';\nimport { getRootPackageJsonPath } from './resolvePackageJson';\n\ntype SplitConfigs = { expo?: ExpoConfig; mods?: ModConfig };\n\nlet hasWarnedAboutRootConfig = false;\n\n/**\n * If a config has an `expo` object then that will be used as the config.\n * This method reduces out other top level values if an `expo` object exists.\n *\n * @param config Input config object to reduce\n */\nfunction reduceExpoObject(config?: any): SplitConfigs | null {\n if (!config) return config || null;\n\n if (config.expo && !hasWarnedAboutRootConfig) {\n const keys = Object.keys(config).filter((key) => key !== 'expo');\n if (keys.length) {\n hasWarnedAboutRootConfig = true;\n const ansiYellow = (str: string) => `\\u001B[33m${str}\\u001B[0m`;\n const ansiGray = (str: string) => `\\u001B[90m${str}\\u001B[0m`;\n const ansiBold = (str: string) => `\\u001B[1m${str}\\u001B[22m`;\n const plural = keys.length > 1;\n console.warn(\n ansiYellow(\n ansiBold('Warning: ') +\n `Root-level ${ansiBold(`\"expo\"`)} object found. Ignoring extra key${plural ? 's' : ''} in Expo config: ${keys\n .map((key) => `\"${key}\"`)\n .join(', ')}\\n` +\n ansiGray(`Learn more: https://expo.fyi/root-expo-object`)\n )\n );\n }\n }\n\n const { mods, ...expo } = config.expo ?? config;\n\n return {\n expo,\n mods,\n };\n}\n\n/**\n * Get all platforms that a project is currently capable of running.\n *\n * @param projectRoot\n * @param exp\n */\nfunction getSupportedPlatforms(projectRoot: string): Platform[] {\n const platforms: Platform[] = [];\n if (resolveFrom.silent(projectRoot, 'react-native')) {\n platforms.push('ios', 'android');\n }\n if (resolveFrom.silent(projectRoot, 'react-dom')) {\n platforms.push('web');\n }\n return platforms;\n}\n\n/**\n * Evaluate the config for an Expo project.\n * If a function is exported from the `app.config.js` then a partial config will be passed as an argument.\n * The partial config is composed from any existing app.json, and certain fields from the `package.json` like name and description.\n *\n * If options.isPublicConfig is true, the Expo config will include only public-facing options (omitting private keys).\n * The resulting config should be suitable for hosting or embedding in a publicly readable location.\n *\n * **Example**\n * ```js\n * module.exports = function({ config }) {\n * // mutate the config before returning it.\n * config.slug = 'new slug'\n * return { expo: config };\n * }\n * ```\n *\n * **Supports**\n * - `app.config.ts`\n * - `app.config.js`\n * - `app.config.json`\n * - `app.json`\n *\n * @param projectRoot the root folder containing all of your application code\n * @param options enforce criteria for a project config\n */\nexport function getConfig(projectRoot: string, options: GetConfigOptions = {}): ProjectConfig {\n const paths = getConfigFilePaths(projectRoot);\n\n const rawStaticConfig = paths.staticConfigPath ? getStaticConfig(paths.staticConfigPath) : null;\n // For legacy reasons, always return an object.\n const rootConfig = (rawStaticConfig || {}) as AppJSONConfig;\n const staticConfig = reduceExpoObject(rawStaticConfig) || {};\n\n // Can only change the package.json location if an app.json or app.config.json exists\n const [packageJson, packageJsonPath] = getPackageJsonAndPath(projectRoot);\n\n function fillAndReturnConfig(\n config: SplitConfigs,\n dynamicConfigObjectType: string | null,\n mayHaveUnusedStaticConfig: boolean = false\n ) {\n const configWithDefaultValues = {\n ...ensureConfigHasDefaultValues({\n projectRoot,\n exp: config.expo || {},\n pkg: packageJson,\n skipSDKVersionRequirement: options.skipSDKVersionRequirement,\n paths,\n packageJsonPath,\n }),\n mods: config.mods,\n dynamicConfigObjectType,\n rootConfig,\n dynamicConfigPath: paths.dynamicConfigPath,\n staticConfigPath: paths.staticConfigPath,\n hasUnusedStaticConfig:\n !!paths.staticConfigPath && !!paths.dynamicConfigPath && mayHaveUnusedStaticConfig,\n };\n\n if (options.isModdedConfig) {\n // @ts-ignore: Add the mods back to the object.\n configWithDefaultValues.exp.mods = config.mods ?? null;\n }\n\n // Apply static json plugins, should be done after _internal\n configWithDefaultValues.exp = withConfigPlugins(\n configWithDefaultValues.exp,\n !!options.skipPlugins\n );\n\n if (!options.isModdedConfig) {\n // @ts-ignore: Delete mods added by static plugins when they won't have a chance to be evaluated\n delete configWithDefaultValues.exp.mods;\n }\n\n if (options.isPublicConfig) {\n // TODD(EvanBacon): Drop plugins array after it's been resolved.\n\n // Remove internal values with references to user's file paths from the public config.\n delete configWithDefaultValues.exp._internal;\n\n // hooks no longer exists in the typescript type but should still be removed\n if ('hooks' in configWithDefaultValues.exp) {\n delete configWithDefaultValues.exp.hooks;\n }\n if (configWithDefaultValues.exp.ios?.config) {\n delete configWithDefaultValues.exp.ios.config;\n }\n if (configWithDefaultValues.exp.android?.config) {\n delete configWithDefaultValues.exp.android.config;\n }\n\n delete configWithDefaultValues.exp.updates?.codeSigningCertificate;\n delete configWithDefaultValues.exp.updates?.codeSigningMetadata;\n }\n\n return configWithDefaultValues;\n }\n\n // Fill in the static config\n function getContextConfig(config: SplitConfigs) {\n return ensureConfigHasDefaultValues({\n projectRoot,\n exp: config.expo || {},\n pkg: packageJson,\n skipSDKVersionRequirement: true,\n paths,\n packageJsonPath,\n }).exp;\n }\n\n if (paths.dynamicConfigPath) {\n // No app.config.json or app.json but app.config.js\n const {\n exportedObjectType,\n config: rawDynamicConfig,\n mayHaveUnusedStaticConfig,\n } = getDynamicConfig(paths.dynamicConfigPath, {\n projectRoot,\n staticConfigPath: paths.staticConfigPath,\n packageJsonPath,\n config: getContextConfig(staticConfig),\n });\n // Allow for the app.config.js to `export default null;`\n // Use `dynamicConfigPath` to detect if a dynamic config exists.\n const dynamicConfig = reduceExpoObject(rawDynamicConfig) || {};\n return fillAndReturnConfig(dynamicConfig, exportedObjectType, mayHaveUnusedStaticConfig);\n }\n\n // No app.config.js but json or no config\n return fillAndReturnConfig(staticConfig || {}, null);\n}\n\nexport function getPackageJson(projectRoot: string): PackageJSONConfig {\n const [pkg] = getPackageJsonAndPath(projectRoot);\n return pkg;\n}\n\nfunction getPackageJsonAndPath(projectRoot: string): [PackageJSONConfig, string] {\n const packageJsonPath = getRootPackageJsonPath(projectRoot);\n return [JsonFile.read(packageJsonPath), packageJsonPath];\n}\n\n/**\n * Get the static and dynamic config paths for a project. Also accounts for custom paths.\n *\n * @param projectRoot\n */\nexport function getConfigFilePaths(projectRoot: string): ConfigFilePaths {\n return {\n dynamicConfigPath: getDynamicConfigFilePath(projectRoot),\n staticConfigPath: getStaticConfigFilePath(projectRoot),\n };\n}\n\nconst DYNAMIC_CONFIG_EXTS = ['.ts', '.mts', '.cts', '.mjs', '.cjs', '.js'];\n\nfunction getDynamicConfigFilePath(projectRoot: string): string | null {\n const fileNames = DYNAMIC_CONFIG_EXTS.map((ext) => `app.config${ext}`);\n for (const fileName of fileNames) {\n const configPath = path.join(projectRoot, fileName);\n try {\n const stat = fs.statSync(configPath);\n if (stat.isFile()) {\n return configPath;\n }\n } catch {}\n }\n return null;\n}\n\nfunction getStaticConfigFilePath(projectRoot: string): string | null {\n for (const fileName of ['app.config.json', 'app.json']) {\n const configPath = path.join(projectRoot, fileName);\n try {\n const stat = fs.statSync(configPath);\n if (stat.isFile()) {\n return configPath;\n }\n } catch {}\n }\n return null;\n}\n\n/**\n * Attempt to modify an Expo project config.\n * This will only fully work if the project is using static configs only.\n * Otherwise 'warn' | 'fail' will return with a message about why the config couldn't be updated.\n * The potentially modified config object will be returned for testing purposes.\n *\n * @param projectRoot\n * @param modifications modifications to make to an existing config\n * @param readOptions options for reading the current config file\n * @param writeOptions If true, the static config file will not be rewritten\n */\nexport async function modifyConfigAsync(\n projectRoot: string,\n modifications: Partial,\n readOptions: GetConfigOptions = {},\n writeOptions: WriteConfigOptions = {}\n): Promise<{\n type: 'success' | 'warn' | 'fail';\n message?: string;\n config: ExpoConfig | null;\n}> {\n const config = getConfig(projectRoot, readOptions);\n const isDryRun = writeOptions.dryRun;\n\n // Create or modify the static config, when not using dynamic config\n if (!config.dynamicConfigPath) {\n const outputConfig = mergeConfigModifications(config, modifications);\n\n if (!isDryRun) {\n const configPath = config.staticConfigPath ?? path.join(projectRoot, 'app.json');\n await JsonFile.writeAsync(configPath, outputConfig, { json5: false });\n }\n\n return { type: 'success', config: outputConfig.expo ?? outputConfig };\n }\n\n // Attempt to write to a function-like dynamic config, when used with a static config\n if (\n config.staticConfigPath &&\n config.dynamicConfigObjectType === 'function' &&\n !modifications.hasOwnProperty('plugins') // We don't know what plugins are in dynamic configs\n ) {\n const outputConfig = mergeConfigModifications(config, modifications);\n\n if (isDryRun) {\n return {\n type: 'warn',\n message: `Cannot verify config modifications in dry-run mode for config at: ${path.relative(projectRoot, config.dynamicConfigPath)}`,\n config: null,\n };\n }\n\n // Attempt to write the static config with the config modifications\n await JsonFile.writeAsync(config.staticConfigPath, outputConfig, { json5: false });\n\n // Verify that the dynamic config is using the static config\n const newConfig = getConfig(projectRoot, readOptions);\n const newConfighasModifications = isMatchingObject(modifications, newConfig.exp);\n if (newConfighasModifications) {\n return {\n type: 'success',\n config: newConfig.exp,\n };\n }\n\n // Rollback the changes when the reloaded config did not include the modifications\n await JsonFile.writeAsync(config.staticConfigPath, config.rootConfig, { json5: false });\n }\n\n // We cannot automatically write to a dynamic config\n return {\n type: 'warn',\n message: `Cannot automatically write to dynamic config at: ${path.relative(\n projectRoot,\n config.dynamicConfigPath\n )}`,\n config: null,\n };\n}\n\n/**\n * Merge the config modifications, using an optional possible top-level `expo` object.\n * Note, changes in the plugins are merged differently to avoid duplicate entries.\n */\nfunction mergeConfigModifications(\n config: ProjectConfig,\n { plugins, ...modifications }: Partial\n): AppJSONConfig {\n const modifiedExpoConfig: ExpoConfig = !config.rootConfig.expo\n ? deepMerge(config.rootConfig, modifications)\n : deepMerge(config.rootConfig.expo, modifications);\n\n if (plugins?.length) {\n // When adding plugins, ensure the config has a plugin list\n if (!modifiedExpoConfig.plugins) {\n modifiedExpoConfig.plugins = [];\n }\n\n // Create a plugin lookup map\n const existingPlugins: Record = Object.fromEntries(\n modifiedExpoConfig.plugins.map((definition) =>\n typeof definition === 'string' ? [definition, undefined] : definition\n )\n );\n\n for (const plugin of plugins) {\n // Unpack the plugin definition, using either the short (string) or normal (array) notation\n const [pluginName, pluginProps] = Array.isArray(plugin) ? plugin : [plugin];\n // Abort if the plugin definition is empty\n if (!pluginName) continue;\n\n // Add the plugin if it doesn't exist yet, including its properties\n if (!(pluginName in existingPlugins)) {\n modifiedExpoConfig.plugins.push(plugin);\n continue;\n }\n\n // If the plugin has properties, and it exists, merge the properties\n if (pluginProps) {\n modifiedExpoConfig.plugins = modifiedExpoConfig.plugins.map((existingPlugin) => {\n const [existingPluginName] = Array.isArray(existingPlugin)\n ? existingPlugin\n : [existingPlugin];\n\n // Do not modify other plugins\n if (existingPluginName !== pluginName) {\n return existingPlugin;\n }\n\n // Add the props to the existing plugin entry\n if (typeof existingPlugin === 'string') {\n return [existingPlugin, pluginProps];\n }\n\n // Merge the props to the existing plugin properties\n if (Array.isArray(existingPlugin) && existingPlugin[0]) {\n return [existingPlugin[0], deepMerge(existingPlugin[1] ?? {}, pluginProps)];\n }\n\n return existingPlugin;\n });\n continue;\n }\n\n // If the same plugin exists with properties, and the modification does not contain properties, ignore\n }\n }\n\n const finalizedConfig = !config.rootConfig.expo\n ? modifiedExpoConfig\n : { ...config.rootConfig, expo: modifiedExpoConfig };\n\n return finalizedConfig as AppJSONConfig;\n}\n\nfunction isMatchingObject>(\n expectedValues: T,\n actualValues: T\n): boolean {\n for (const key in expectedValues) {\n if (!expectedValues.hasOwnProperty(key)) {\n continue;\n }\n\n if (typeof expectedValues[key] === 'object' && actualValues[key] !== null) {\n if (!isMatchingObject(expectedValues[key], actualValues[key])) {\n return false;\n }\n } else {\n if (expectedValues[key] !== actualValues[key]) {\n return false;\n }\n }\n }\n return true;\n}\n\nfunction ensureConfigHasDefaultValues({\n projectRoot,\n exp,\n pkg,\n paths,\n packageJsonPath,\n skipSDKVersionRequirement = false,\n}: {\n projectRoot: string;\n exp: Partial | null;\n pkg: JSONObject;\n skipSDKVersionRequirement?: boolean;\n paths?: ConfigFilePaths;\n packageJsonPath?: string;\n}): { exp: ExpoConfig; pkg: PackageJSONConfig } {\n if (!exp) {\n exp = {};\n }\n exp = withInternal(exp as any, {\n projectRoot,\n ...(paths ?? {}),\n packageJsonPath,\n });\n // Defaults for package.json fields\n const pkgName = typeof pkg.name === 'string' ? pkg.name : path.basename(projectRoot);\n const pkgVersion = typeof pkg.version === 'string' ? pkg.version : '1.0.0';\n\n const pkgWithDefaults = { ...pkg, name: pkgName, version: pkgVersion };\n\n // Defaults for app.json/app.config.js fields\n const name = exp.name ?? pkgName;\n const slug = exp.slug ?? slugify(name.toLowerCase());\n const version = exp.version ?? pkgVersion;\n let description = exp.description;\n if (!description && typeof pkg.description === 'string') {\n description = pkg.description;\n }\n\n const expWithDefaults = { ...exp, name, slug, version, description };\n\n let sdkVersion;\n try {\n sdkVersion = getExpoSDKVersion(projectRoot, expWithDefaults);\n } catch (error) {\n if (!skipSDKVersionRequirement) throw error;\n }\n\n let platforms = exp.platforms;\n if (!platforms) {\n platforms = getSupportedPlatforms(projectRoot);\n }\n\n return {\n exp: { ...expWithDefaults, sdkVersion, platforms },\n pkg: pkgWithDefaults,\n };\n}\n\nconst DEFAULT_BUILD_PATH = `web-build`;\n\nexport function getWebOutputPath(config: { [key: string]: any } = {}): string {\n if (process.env.WEBPACK_BUILD_OUTPUT_PATH) {\n return process.env.WEBPACK_BUILD_OUTPUT_PATH;\n }\n const expo = config.expo || config || {};\n return expo?.web?.build?.output || DEFAULT_BUILD_PATH;\n}\n\nexport function getNameFromConfig(exp: Record = {}): {\n appName?: string;\n webName?: string;\n} {\n // For RN CLI support\n const appManifest = exp.expo || exp;\n const { web = {} } = appManifest;\n\n // rn-cli apps use a displayName value as well.\n const appName = exp.displayName || appManifest.displayName || appManifest.name;\n const webName = web.name || appName;\n\n return {\n appName,\n webName,\n };\n}\n\nexport function getDefaultTarget(\n projectRoot: string,\n exp?: Pick\n): ProjectTarget {\n exp ??= getConfig(projectRoot, { skipSDKVersionRequirement: true }).exp;\n\n // before SDK 37, always default to managed to preserve previous behavior\n if (exp.sdkVersion && exp.sdkVersion !== 'UNVERSIONED' && semver.lt(exp.sdkVersion, '37.0.0')) {\n return 'managed';\n }\n return isBareWorkflowProject(projectRoot) ? 'bare' : 'managed';\n}\n\nfunction isBareWorkflowProject(projectRoot: string): boolean {\n const [pkg] = getPackageJsonAndPath(projectRoot);\n\n // TODO: Drop this\n if (pkg.dependencies && pkg.dependencies.expokit) {\n return false;\n }\n\n const xcodeprojFiles = globSync('ios/**/*.xcodeproj', {\n absolute: true,\n cwd: projectRoot,\n });\n if (xcodeprojFiles.length) {\n return true;\n }\n const gradleFiles = globSync('android/**/*.gradle', {\n absolute: true,\n cwd: projectRoot,\n });\n if (gradleFiles.length) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Return a useful name describing the project config.\n * - dynamic: app.config.js\n * - static: app.json\n * - custom path app config relative to root folder\n * - both: app.config.js or app.json\n */\nexport function getProjectConfigDescription(projectRoot: string): string {\n const paths = getConfigFilePaths(projectRoot);\n return getProjectConfigDescriptionWithPaths(projectRoot, paths);\n}\n\n/**\n * Returns a string describing the configurations used for the given project root.\n * Will return null if no config is found.\n *\n * @param projectRoot\n * @param projectConfig\n */\nexport function getProjectConfigDescriptionWithPaths(\n projectRoot: string,\n projectConfig: ConfigFilePaths\n): string {\n if (projectConfig.dynamicConfigPath) {\n const relativeDynamicConfigPath = path.relative(projectRoot, projectConfig.dynamicConfigPath);\n if (projectConfig.staticConfigPath) {\n return `${relativeDynamicConfigPath} or ${path.relative(\n projectRoot,\n projectConfig.staticConfigPath\n )}`;\n }\n return relativeDynamicConfigPath;\n } else if (projectConfig.staticConfigPath) {\n return path.relative(projectRoot, projectConfig.staticConfigPath);\n }\n // If a config doesn't exist, our tooling will generate a static app.json\n return 'app.json';\n}\n\nexport * from './Config.types';\n"],"mappings":"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","ignoreList":[]} \ No newline at end of file diff --git a/packages/@expo/config/src/Config.ts b/packages/@expo/config/src/Config.ts index 76188c73748c4e..e9db149f9ccb6a 100644 --- a/packages/@expo/config/src/Config.ts +++ b/packages/@expo/config/src/Config.ts @@ -239,12 +239,18 @@ export function getConfigFilePaths(projectRoot: string): ConfigFilePaths { }; } +const DYNAMIC_CONFIG_EXTS = ['.ts', '.mts', '.cts', '.mjs', '.cjs', '.js']; + function getDynamicConfigFilePath(projectRoot: string): string | null { - for (const fileName of ['app.config.ts', 'app.config.js']) { + const fileNames = DYNAMIC_CONFIG_EXTS.map((ext) => `app.config${ext}`); + for (const fileName of fileNames) { const configPath = path.join(projectRoot, fileName); - if (fs.existsSync(configPath)) { - return configPath; - } + try { + const stat = fs.statSync(configPath); + if (stat.isFile()) { + return configPath; + } + } catch {} } return null; } @@ -252,9 +258,12 @@ function getDynamicConfigFilePath(projectRoot: string): string | null { function getStaticConfigFilePath(projectRoot: string): string | null { for (const fileName of ['app.config.json', 'app.json']) { const configPath = path.join(projectRoot, fileName); - if (fs.existsSync(configPath)) { - return configPath; - } + try { + const stat = fs.statSync(configPath); + if (stat.isFile()) { + return configPath; + } + } catch {} } return null; } diff --git a/packages/@expo/require-utils/CHANGELOG.md b/packages/@expo/require-utils/CHANGELOG.md index a98674bacbf288..80d34b12939b5f 100644 --- a/packages/@expo/require-utils/CHANGELOG.md +++ b/packages/@expo/require-utils/CHANGELOG.md @@ -8,6 +8,8 @@ ### 🐛 Bug fixes +- Support CommonJS syntax in `.ts` evaluated files ([#43243](https://github.com/expo/expo/pull/43242)) + ### 💡 Others ## 55.0.0 — 2026-02-16 diff --git a/packages/@expo/require-utils/build/load.js b/packages/@expo/require-utils/build/load.js index 4c0c2772a6fabc..d75eb1f196b738 100644 --- a/packages/@expo/require-utils/build/load.js +++ b/packages/@expo/require-utils/build/load.js @@ -120,15 +120,30 @@ function evalModule(code, filename, opts = {}) { const ext = _nodePath().default.extname(filename); const ts = loadTypescript(); if (ts) { + let module; + if (ext === '.cts') { + module = ts.ModuleKind.CommonJS; + } else if (ext === '.mts') { + module = ts.ModuleKind.ESNext; + } else { + // NOTE(@kitten): We can "preserve" the output, meaning, it can either be ESM or CJS + // and stop TypeScript from either transpiling it to CommonJS or adding an `export {}` + // if no exports are used. This allows the user to choose if this file is CJS or ESM + // (but not to mix both) + module = ts.ModuleKind.Preserve; + } const output = ts.transpileModule(code, { fileName: filename, reportDiagnostics: true, compilerOptions: { - module: ext === '.cts' ? ts.ModuleKind.CommonJS : ts.ModuleKind.ESNext, + module, moduleResolution: ts.ModuleResolutionKind.Bundler, + // `verbatimModuleSyntax` needs to be off, to erase as many imports as possible + verbatimModuleSyntax: false, target: ts.ScriptTarget.ESNext, newLine: ts.NewLineKind.LineFeed, - inlineSourceMap: true + inlineSourceMap: true, + esModuleInterop: true } }); inputCode = output?.outputText || inputCode; diff --git a/packages/@expo/require-utils/build/load.js.map b/packages/@expo/require-utils/build/load.js.map index 93eb47f9261462..7aae616cda3c25 100644 --- a/packages/@expo/require-utils/build/load.js.map +++ b/packages/@expo/require-utils/build/load.js.map @@ -1 +1 @@ -{"version":3,"file":"load.js","names":["_nodeFs","data","_interopRequireDefault","require","nodeModule","_interopRequireWildcard","_nodePath","_nodeUrl","_codeframe","e","__esModule","default","_getRequireWildcardCache","WeakMap","r","t","has","get","n","__proto__","a","Object","defineProperty","getOwnPropertyDescriptor","u","hasOwnProperty","call","i","set","_ts","loadTypescript","undefined","error","code","parent","module","tsExtensionMapping","toFormat","filename","endsWith","isTypescriptFilename","compileModule","opts","format","prependPaths","paths","nodeModulePaths","_nodeModulePaths","path","dirname","mod","assign","Module","_compile","cache","children","splice","indexOf","exports","hasStripTypeScriptTypes","stripTypeScriptTypes","evalModule","inputCode","inputFilename","diagnostic","ext","extname","ts","output","transpileModule","fileName","reportDiagnostics","compilerOptions","ModuleKind","CommonJS","ESNext","moduleResolution","ModuleResolutionKind","Bundler","target","ScriptTarget","newLine","NewLineKind","LineFeed","inlineSourceMap","outputText","diagnostics","length","mode","sourceMap","inputExt","join","basename","formatDiagnostic","annotateError","requireOrImport","Promise","resolve","isAbsolute","url","pathToFileURL","toString","then","s","loadModule","loadModuleSync","fs","readFileSync"],"sources":["../src/load.ts"],"sourcesContent":["import fs from 'node:fs';\nimport * as nodeModule from 'node:module';\nimport path from 'node:path';\nimport url from 'node:url';\nimport type * as ts from 'typescript';\n\nimport { annotateError, formatDiagnostic } from './codeframe';\n\ndeclare module 'node:module' {\n export function _nodeModulePaths(base: string): readonly string[];\n}\n\ndeclare global {\n namespace NodeJS {\n export interface Module {\n _compile(\n code: string,\n filename: string,\n format?: 'module' | 'commonjs' | 'commonjs-typescript' | 'module-typescript' | 'typescript'\n ): unknown;\n }\n }\n}\n\nlet _ts: typeof import('typescript') | null | undefined;\nfunction loadTypescript() {\n if (_ts === undefined) {\n try {\n _ts = require('typescript');\n } catch (error: any) {\n if (error.code !== 'MODULE_NOT_FOUND') {\n throw error;\n } else {\n _ts = null;\n }\n }\n }\n return _ts;\n}\n\nconst parent = module;\n\nconst tsExtensionMapping: Record = {\n '.ts': '.js',\n '.cts': '.cjs',\n '.mts': '.mjs',\n};\n\nfunction toFormat(filename: string) {\n if (filename.endsWith('.cjs')) {\n return 'commonjs';\n } else if (filename.endsWith('.mjs')) {\n return 'module';\n } else if (filename.endsWith('.js')) {\n return undefined;\n } else if (filename.endsWith('.mts')) {\n return 'module-typescript';\n } else if (filename.endsWith('.cts')) {\n return 'commonjs-typescript';\n } else if (filename.endsWith('.ts')) {\n return 'typescript';\n } else {\n return undefined;\n }\n}\n\nfunction isTypescriptFilename(filename: string) {\n switch (toFormat(filename)) {\n case 'module-typescript':\n case 'commonjs-typescript':\n case 'typescript':\n return true;\n default:\n return false;\n }\n}\n\nexport interface ModuleOptions {\n paths?: string[];\n}\n\nfunction compileModule(code: string, filename: string, opts: ModuleOptions): T {\n const format = toFormat(filename);\n const prependPaths = opts.paths ?? [];\n const nodeModulePaths = nodeModule._nodeModulePaths(path.dirname(filename));\n const paths = [...prependPaths, ...nodeModulePaths];\n try {\n const mod = Object.assign(new nodeModule.Module(filename, parent), { filename, paths });\n mod._compile(code, filename, format);\n require.cache[filename] = mod;\n parent?.children?.splice(parent.children.indexOf(mod), 1);\n return mod.exports;\n } catch (error: any) {\n delete require.cache[filename];\n throw error;\n }\n}\n\nconst hasStripTypeScriptTypes = typeof nodeModule.stripTypeScriptTypes === 'function';\n\nfunction evalModule(code: string, filename: string, opts: ModuleOptions = {}) {\n let inputCode = code;\n let inputFilename = filename;\n let diagnostic: ts.Diagnostic | undefined;\n if (filename.endsWith('.ts') || filename.endsWith('.cts') || filename.endsWith('.mts')) {\n const ext = path.extname(filename);\n const ts = loadTypescript();\n\n if (ts) {\n const output = ts.transpileModule(code, {\n fileName: filename,\n reportDiagnostics: true,\n compilerOptions: {\n module: ext === '.cts' ? ts.ModuleKind.CommonJS : ts.ModuleKind.ESNext,\n moduleResolution: ts.ModuleResolutionKind.Bundler,\n target: ts.ScriptTarget.ESNext,\n newLine: ts.NewLineKind.LineFeed,\n inlineSourceMap: true,\n },\n });\n inputCode = output?.outputText || inputCode;\n if (output?.diagnostics?.length) {\n diagnostic = output.diagnostics[0];\n }\n }\n\n if (hasStripTypeScriptTypes && inputCode === code) {\n // This may throw its own error, but this contains a code-frame already\n inputCode = nodeModule.stripTypeScriptTypes(code, {\n mode: 'transform',\n sourceMap: true,\n });\n }\n\n if (inputCode !== code) {\n const inputExt = tsExtensionMapping[ext] ?? ext;\n if (inputExt !== ext) {\n inputFilename = path.join(path.dirname(filename), path.basename(filename, ext) + inputExt);\n }\n }\n }\n\n try {\n const mod = compileModule(inputCode, inputFilename, opts);\n if (inputFilename !== filename) {\n require.cache[filename] = mod;\n }\n return mod;\n } catch (error: any) {\n // If we have a diagnostic from TypeScript, we issue its error with a codeframe first,\n // since it's likely more useful than the eval error\n throw formatDiagnostic(diagnostic) ?? annotateError(code, filename, error) ?? error;\n }\n}\n\nasync function requireOrImport(filename: string) {\n try {\n return require(filename);\n } catch {\n return await import(\n path.isAbsolute(filename) ? url.pathToFileURL(filename).toString() : filename\n );\n }\n}\n\nasync function loadModule(filename: string) {\n try {\n return await requireOrImport(filename);\n } catch (error: any) {\n if (error.code === 'ERR_UNKNOWN_FILE_EXTENSION' || error.code === 'MODULE_NOT_FOUND') {\n return loadModuleSync(filename);\n } else {\n throw error;\n }\n }\n}\n\n/** Require module or evaluate with TypeScript\n * NOTE: Requiring ESM has been added in all LTS versions (Node 20.19+, 22.12+, 24).\n * This already forms the minimum required Node version as of Expo SDK 54 */\nfunction loadModuleSync(filename: string) {\n try {\n if (!isTypescriptFilename(filename)) {\n return require(filename);\n }\n } catch (error: any) {\n if (error.code === 'MODULE_NOT_FOUND') {\n throw error;\n }\n // We fallback to always evaluating the entrypoint module\n // This is out of safety, since we're not trusting the requiring ESM feature\n // and evaluating the module manually bypasses the error when it's flagged off\n }\n const code = fs.readFileSync(filename, 'utf8');\n return evalModule(code, filename);\n}\n\nexport { evalModule, loadModule, loadModuleSync };\n"],"mappings":"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","ignoreList":[]} \ No newline at end of file +{"version":3,"file":"load.js","names":["_nodeFs","data","_interopRequireDefault","require","nodeModule","_interopRequireWildcard","_nodePath","_nodeUrl","_codeframe","e","__esModule","default","_getRequireWildcardCache","WeakMap","r","t","has","get","n","__proto__","a","Object","defineProperty","getOwnPropertyDescriptor","u","hasOwnProperty","call","i","set","_ts","loadTypescript","undefined","error","code","parent","module","tsExtensionMapping","toFormat","filename","endsWith","isTypescriptFilename","compileModule","opts","format","prependPaths","paths","nodeModulePaths","_nodeModulePaths","path","dirname","mod","assign","Module","_compile","cache","children","splice","indexOf","exports","hasStripTypeScriptTypes","stripTypeScriptTypes","evalModule","inputCode","inputFilename","diagnostic","ext","extname","ts","ModuleKind","CommonJS","ESNext","Preserve","output","transpileModule","fileName","reportDiagnostics","compilerOptions","moduleResolution","ModuleResolutionKind","Bundler","verbatimModuleSyntax","target","ScriptTarget","newLine","NewLineKind","LineFeed","inlineSourceMap","esModuleInterop","outputText","diagnostics","length","mode","sourceMap","inputExt","join","basename","formatDiagnostic","annotateError","requireOrImport","Promise","resolve","isAbsolute","url","pathToFileURL","toString","then","s","loadModule","loadModuleSync","fs","readFileSync"],"sources":["../src/load.ts"],"sourcesContent":["import fs from 'node:fs';\nimport * as nodeModule from 'node:module';\nimport path from 'node:path';\nimport url from 'node:url';\nimport type * as ts from 'typescript';\n\nimport { annotateError, formatDiagnostic } from './codeframe';\n\ndeclare module 'node:module' {\n export function _nodeModulePaths(base: string): readonly string[];\n}\n\ndeclare global {\n namespace NodeJS {\n export interface Module {\n _compile(\n code: string,\n filename: string,\n format?: 'module' | 'commonjs' | 'commonjs-typescript' | 'module-typescript' | 'typescript'\n ): unknown;\n }\n }\n}\n\nlet _ts: typeof import('typescript') | null | undefined;\nfunction loadTypescript() {\n if (_ts === undefined) {\n try {\n _ts = require('typescript');\n } catch (error: any) {\n if (error.code !== 'MODULE_NOT_FOUND') {\n throw error;\n } else {\n _ts = null;\n }\n }\n }\n return _ts;\n}\n\nconst parent = module;\n\nconst tsExtensionMapping: Record = {\n '.ts': '.js',\n '.cts': '.cjs',\n '.mts': '.mjs',\n};\n\nfunction toFormat(filename: string) {\n if (filename.endsWith('.cjs')) {\n return 'commonjs';\n } else if (filename.endsWith('.mjs')) {\n return 'module';\n } else if (filename.endsWith('.js')) {\n return undefined;\n } else if (filename.endsWith('.mts')) {\n return 'module-typescript';\n } else if (filename.endsWith('.cts')) {\n return 'commonjs-typescript';\n } else if (filename.endsWith('.ts')) {\n return 'typescript';\n } else {\n return undefined;\n }\n}\n\nfunction isTypescriptFilename(filename: string) {\n switch (toFormat(filename)) {\n case 'module-typescript':\n case 'commonjs-typescript':\n case 'typescript':\n return true;\n default:\n return false;\n }\n}\n\nexport interface ModuleOptions {\n paths?: string[];\n}\n\nfunction compileModule(code: string, filename: string, opts: ModuleOptions): T {\n const format = toFormat(filename);\n const prependPaths = opts.paths ?? [];\n const nodeModulePaths = nodeModule._nodeModulePaths(path.dirname(filename));\n const paths = [...prependPaths, ...nodeModulePaths];\n try {\n const mod = Object.assign(new nodeModule.Module(filename, parent), { filename, paths });\n mod._compile(code, filename, format);\n require.cache[filename] = mod;\n parent?.children?.splice(parent.children.indexOf(mod), 1);\n return mod.exports;\n } catch (error: any) {\n delete require.cache[filename];\n throw error;\n }\n}\n\nconst hasStripTypeScriptTypes = typeof nodeModule.stripTypeScriptTypes === 'function';\n\nfunction evalModule(code: string, filename: string, opts: ModuleOptions = {}) {\n let inputCode = code;\n let inputFilename = filename;\n let diagnostic: ts.Diagnostic | undefined;\n if (filename.endsWith('.ts') || filename.endsWith('.cts') || filename.endsWith('.mts')) {\n const ext = path.extname(filename);\n const ts = loadTypescript();\n\n if (ts) {\n let module: ts.ModuleKind;\n if (ext === '.cts') {\n module = ts.ModuleKind.CommonJS;\n } else if (ext === '.mts') {\n module = ts.ModuleKind.ESNext;\n } else {\n // NOTE(@kitten): We can \"preserve\" the output, meaning, it can either be ESM or CJS\n // and stop TypeScript from either transpiling it to CommonJS or adding an `export {}`\n // if no exports are used. This allows the user to choose if this file is CJS or ESM\n // (but not to mix both)\n module = ts.ModuleKind.Preserve;\n }\n const output = ts.transpileModule(code, {\n fileName: filename,\n reportDiagnostics: true,\n compilerOptions: {\n module,\n moduleResolution: ts.ModuleResolutionKind.Bundler,\n // `verbatimModuleSyntax` needs to be off, to erase as many imports as possible\n verbatimModuleSyntax: false,\n target: ts.ScriptTarget.ESNext,\n newLine: ts.NewLineKind.LineFeed,\n inlineSourceMap: true,\n esModuleInterop: true,\n },\n });\n inputCode = output?.outputText || inputCode;\n if (output?.diagnostics?.length) {\n diagnostic = output.diagnostics[0];\n }\n }\n\n if (hasStripTypeScriptTypes && inputCode === code) {\n // This may throw its own error, but this contains a code-frame already\n inputCode = nodeModule.stripTypeScriptTypes(code, {\n mode: 'transform',\n sourceMap: true,\n });\n }\n\n if (inputCode !== code) {\n const inputExt = tsExtensionMapping[ext] ?? ext;\n if (inputExt !== ext) {\n inputFilename = path.join(path.dirname(filename), path.basename(filename, ext) + inputExt);\n }\n }\n }\n\n try {\n const mod = compileModule(inputCode, inputFilename, opts);\n if (inputFilename !== filename) {\n require.cache[filename] = mod;\n }\n return mod;\n } catch (error: any) {\n // If we have a diagnostic from TypeScript, we issue its error with a codeframe first,\n // since it's likely more useful than the eval error\n throw formatDiagnostic(diagnostic) ?? annotateError(code, filename, error) ?? error;\n }\n}\n\nasync function requireOrImport(filename: string) {\n try {\n return require(filename);\n } catch {\n return await import(\n path.isAbsolute(filename) ? url.pathToFileURL(filename).toString() : filename\n );\n }\n}\n\nasync function loadModule(filename: string) {\n try {\n return await requireOrImport(filename);\n } catch (error: any) {\n if (error.code === 'ERR_UNKNOWN_FILE_EXTENSION' || error.code === 'MODULE_NOT_FOUND') {\n return loadModuleSync(filename);\n } else {\n throw error;\n }\n }\n}\n\n/** Require module or evaluate with TypeScript\n * NOTE: Requiring ESM has been added in all LTS versions (Node 20.19+, 22.12+, 24).\n * This already forms the minimum required Node version as of Expo SDK 54 */\nfunction loadModuleSync(filename: string) {\n try {\n if (!isTypescriptFilename(filename)) {\n return require(filename);\n }\n } catch (error: any) {\n if (error.code === 'MODULE_NOT_FOUND') {\n throw error;\n }\n // We fallback to always evaluating the entrypoint module\n // This is out of safety, since we're not trusting the requiring ESM feature\n // and evaluating the module manually bypasses the error when it's flagged off\n }\n const code = fs.readFileSync(filename, 'utf8');\n return evalModule(code, filename);\n}\n\nexport { evalModule, loadModule, loadModuleSync };\n"],"mappings":"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","ignoreList":[]} \ No newline at end of file diff --git a/packages/@expo/require-utils/src/load.ts b/packages/@expo/require-utils/src/load.ts index fd78e0d5affd27..30382356075432 100644 --- a/packages/@expo/require-utils/src/load.ts +++ b/packages/@expo/require-utils/src/load.ts @@ -107,15 +107,30 @@ function evalModule(code: string, filename: string, opts: ModuleOptions = {}) { const ts = loadTypescript(); if (ts) { + let module: ts.ModuleKind; + if (ext === '.cts') { + module = ts.ModuleKind.CommonJS; + } else if (ext === '.mts') { + module = ts.ModuleKind.ESNext; + } else { + // NOTE(@kitten): We can "preserve" the output, meaning, it can either be ESM or CJS + // and stop TypeScript from either transpiling it to CommonJS or adding an `export {}` + // if no exports are used. This allows the user to choose if this file is CJS or ESM + // (but not to mix both) + module = ts.ModuleKind.Preserve; + } const output = ts.transpileModule(code, { fileName: filename, reportDiagnostics: true, compilerOptions: { - module: ext === '.cts' ? ts.ModuleKind.CommonJS : ts.ModuleKind.ESNext, + module, moduleResolution: ts.ModuleResolutionKind.Bundler, + // `verbatimModuleSyntax` needs to be off, to erase as many imports as possible + verbatimModuleSyntax: false, target: ts.ScriptTarget.ESNext, newLine: ts.NewLineKind.LineFeed, inlineSourceMap: true, + esModuleInterop: true, }, }); inputCode = output?.outputText || inputCode; diff --git a/packages/babel-preset-expo/CHANGELOG.md b/packages/babel-preset-expo/CHANGELOG.md index 2e9dc6b1e7171f..f0356374cad74d 100644 --- a/packages/babel-preset-expo/CHANGELOG.md +++ b/packages/babel-preset-expo/CHANGELOG.md @@ -8,6 +8,8 @@ ### 🐛 Bug fixes +- Strip loaders from server bundles ([#43212](https://github.com/expo/expo/pull/43212) by [@hassankhan](https://github.com/hassankhan)) + ### 💡 Others ## 55.0.5 — 2026-02-16 diff --git a/packages/babel-preset-expo/build/server-data-loaders-plugin.js b/packages/babel-preset-expo/build/server-data-loaders-plugin.js index 4164eac4e7d91f..cd94f096480db8 100644 --- a/packages/babel-preset-expo/build/server-data-loaders-plugin.js +++ b/packages/babel-preset-expo/build/server-data-loaders-plugin.js @@ -7,7 +7,6 @@ const LOADER_EXPORT_NAME = 'loader'; function serverDataLoadersPlugin(api) { const { types: t } = api; const routerAbsoluteRoot = api.caller(common_1.getExpoRouterAbsoluteAppRoot); - const isServer = api.caller(common_1.getIsServer); const isLoaderBundle = api.caller(common_1.getIsLoaderBundle); return { name: 'expo-server-data-loaders', @@ -26,11 +25,6 @@ function serverDataLoadersPlugin(api) { path.remove(); }, ExportNamedDeclaration(path, state) { - // NOTE(@hassankhan): Server bundles currently preserve loaders for SSG, a followup is - // required to remove them. - if (isServer && !isLoaderBundle) { - return; - } // Early exit if file is not within the `app/` directory if (!isInAppDirectory(state.file.opts.filename ?? '', routerAbsoluteRoot)) { debug('Skipping file outside app directory:', state.file.opts.filename); diff --git a/packages/babel-preset-expo/src/__tests__/server-data-loaders-plugin.test.ts b/packages/babel-preset-expo/src/__tests__/server-data-loaders-plugin.test.ts index 8ba927c2ef99a5..f59e123f4182e0 100644 --- a/packages/babel-preset-expo/src/__tests__/server-data-loaders-plugin.test.ts +++ b/packages/babel-preset-expo/src/__tests__/server-data-loaders-plugin.test.ts @@ -63,8 +63,10 @@ afterAll(() => { process.env = { ...originalEnv }; }); +type BundleTypes = 'client' | 'server' | 'loader'; + type TransformTestOptions = Partial & { - bundleType: 'client' | 'server' | 'loader'; + bundleType: BundleTypes; }; function transformTest(code: string, { bundleType, ...defaultOverrideOpts }: TransformTestOptions) { @@ -91,11 +93,13 @@ function transformTest(code: string, { bundleType, ...defaultOverrideOpts }: Tra }; } -describe('client', () => { - describe('removes loader exports', () => { - it('removes `export async function loader() {}`', () => { - const res = transformTest( - ` +describe('client and server', () => { + describe.each(['client', 'server'])( + 'removes loader exports for %s bundles', + (bundleType: BundleTypes) => { + it('removes `export async function loader() {}`', () => { + const res = transformTest( + ` import { useLoaderData } from 'expo-router'; export async function loader() { @@ -107,26 +111,26 @@ describe('client', () => { return
{data.data}
; } `, - { bundleType: 'client' } - ); - - expect(res.metadata.performConstantFolding).toBe(true); - expect(res.metadata.loaderReference).toBe('/app/index'); - expect(res.code).toMatchInlineSnapshot(` - "import { useLoaderData } from 'expo-router'; - import { jsx as _jsx } from "react/jsx-runtime"; - export default function Index() { - const data = useLoaderData(); - return /*#__PURE__*/_jsx("div", { - children: data.data - }); - }" - `); - }); - - it('removes `export function loader() {}`', () => { - const res = transformTest( - ` + { bundleType } + ); + + expect(res.metadata.performConstantFolding).toBe(true); + expect(res.metadata.loaderReference).toBe('/app/index'); + expect(res.code).toMatchInlineSnapshot(` + "import { useLoaderData } from 'expo-router'; + import { jsx as _jsx } from "react/jsx-runtime"; + export default function Index() { + const data = useLoaderData(); + return /*#__PURE__*/_jsx("div", { + children: data.data + }); + }" + `); + }); + + it('removes `export function loader() {}`', () => { + const res = transformTest( + ` import { useLoaderData } from 'expo-router'; export function loader() { @@ -138,26 +142,26 @@ describe('client', () => { return
{data.data}
; } `, - { bundleType: 'client' } - ); - - expect(res.metadata.performConstantFolding).toBe(true); - expect(res.metadata.loaderReference).toBe('/app/index'); - expect(res.code).toMatchInlineSnapshot(` - "import { useLoaderData } from 'expo-router'; - import { jsx as _jsx } from "react/jsx-runtime"; - export default function Index() { - const data = useLoaderData(); - return /*#__PURE__*/_jsx("div", { - children: data.data - }); - }" - `); - }); - - it('removes `export const loader = async () => {}`', () => { - const res = transformTest( - ` + { bundleType } + ); + + expect(res.metadata.performConstantFolding).toBe(true); + expect(res.metadata.loaderReference).toBe('/app/index'); + expect(res.code).toMatchInlineSnapshot(` + "import { useLoaderData } from 'expo-router'; + import { jsx as _jsx } from "react/jsx-runtime"; + export default function Index() { + const data = useLoaderData(); + return /*#__PURE__*/_jsx("div", { + children: data.data + }); + }" + `); + }); + + it('removes `export const loader = async () => {}`', () => { + const res = transformTest( + ` import { useLoaderData } from 'expo-router'; export const loader = async () => { @@ -169,26 +173,26 @@ describe('client', () => { return
{data.data}
; } `, - { bundleType: 'client' } - ); - - expect(res.metadata.performConstantFolding).toBe(true); - expect(res.metadata.loaderReference).toBe('/app/index'); - expect(res.code).toMatchInlineSnapshot(` - "import { useLoaderData } from 'expo-router'; - import { jsx as _jsx } from "react/jsx-runtime"; - export default function Index() { - const data = useLoaderData(); - return /*#__PURE__*/_jsx("div", { - children: data.data - }); - }" - `); - }); - - it('removes `export const loader = () => {}`', () => { - const res = transformTest( - ` + { bundleType } + ); + + expect(res.metadata.performConstantFolding).toBe(true); + expect(res.metadata.loaderReference).toBe('/app/index'); + expect(res.code).toMatchInlineSnapshot(` + "import { useLoaderData } from 'expo-router'; + import { jsx as _jsx } from "react/jsx-runtime"; + export default function Index() { + const data = useLoaderData(); + return /*#__PURE__*/_jsx("div", { + children: data.data + }); + }" + `); + }); + + it('removes `export const loader = () => {}`', () => { + const res = transformTest( + ` import { useLoaderData } from 'expo-router'; export const loader = () => { @@ -200,26 +204,26 @@ describe('client', () => { return
{data.data}
; } `, - { bundleType: 'client' } - ); - - expect(res.metadata.performConstantFolding).toBe(true); - expect(res.metadata.loaderReference).toBe('/app/index'); - expect(res.code).toMatchInlineSnapshot(` - "import { useLoaderData } from 'expo-router'; - import { jsx as _jsx } from "react/jsx-runtime"; - export default function Index() { - const data = useLoaderData(); - return /*#__PURE__*/_jsx("div", { - children: data.data - }); - }" - `); - }); - - it('removes `export const loader = async function() {}`', () => { - const res = transformTest( - ` + { bundleType } + ); + + expect(res.metadata.performConstantFolding).toBe(true); + expect(res.metadata.loaderReference).toBe('/app/index'); + expect(res.code).toMatchInlineSnapshot(` + "import { useLoaderData } from 'expo-router'; + import { jsx as _jsx } from "react/jsx-runtime"; + export default function Index() { + const data = useLoaderData(); + return /*#__PURE__*/_jsx("div", { + children: data.data + }); + }" + `); + }); + + it('removes `export const loader = async function() {}`', () => { + const res = transformTest( + ` import { useLoaderData } from 'expo-router'; export const loader = async function() { @@ -231,26 +235,26 @@ describe('client', () => { return
{data.data}
; } `, - { bundleType: 'client' } - ); - - expect(res.metadata.performConstantFolding).toBe(true); - expect(res.metadata.loaderReference).toBe('/app/index'); - expect(res.code).toMatchInlineSnapshot(` - "import { useLoaderData } from 'expo-router'; - import { jsx as _jsx } from "react/jsx-runtime"; - export default function Index() { - const data = useLoaderData(); - return /*#__PURE__*/_jsx("div", { - children: data.data - }); - }" - `); - }); - - it('removes `export const loader = function() {}`', () => { - const res = transformTest( - ` + { bundleType } + ); + + expect(res.metadata.performConstantFolding).toBe(true); + expect(res.metadata.loaderReference).toBe('/app/index'); + expect(res.code).toMatchInlineSnapshot(` + "import { useLoaderData } from 'expo-router'; + import { jsx as _jsx } from "react/jsx-runtime"; + export default function Index() { + const data = useLoaderData(); + return /*#__PURE__*/_jsx("div", { + children: data.data + }); + }" + `); + }); + + it('removes `export const loader = function() {}`', () => { + const res = transformTest( + ` import { useLoaderData } from 'expo-router'; export const loader = function() { @@ -262,24 +266,27 @@ describe('client', () => { return
{data.data}
; } `, - { bundleType: 'client' } - ); - - expect(res.metadata.performConstantFolding).toBe(true); - expect(res.metadata.loaderReference).toBe('/app/index'); - expect(res.code).toMatchInlineSnapshot(` - "import { useLoaderData } from 'expo-router'; - import { jsx as _jsx } from "react/jsx-runtime"; - export default function Index() { - const data = useLoaderData(); - return /*#__PURE__*/_jsx("div", { - children: data.data - }); - }" - `); - }); - }); + { bundleType } + ); + + expect(res.metadata.performConstantFolding).toBe(true); + expect(res.metadata.loaderReference).toBe('/app/index'); + expect(res.code).toMatchInlineSnapshot(` + "import { useLoaderData } from 'expo-router'; + import { jsx as _jsx } from "react/jsx-runtime"; + export default function Index() { + const data = useLoaderData(); + return /*#__PURE__*/_jsx("div", { + children: data.data + }); + }" + `); + }); + } + ); +}); +describe('client', () => { describe('preserves non-loader exports', () => { it('preserves other named exports', () => { const res = transformTest( @@ -303,18 +310,18 @@ describe('client', () => { expect(res.metadata.performConstantFolding).toBe(true); expect(res.metadata.loaderReference).toBe('/app/index'); expect(res.code).toMatchInlineSnapshot(` - "import { useLoaderData } from 'expo-router'; - import { jsx as _jsx } from "react/jsx-runtime"; - export const unstable_settings = { - anchor: 'index' - }; - export default function Index() { - const data = useLoaderData(); - return /*#__PURE__*/_jsx("div", { - children: data.data - }); - }" - `); + "import { useLoaderData } from 'expo-router'; + import { jsx as _jsx } from "react/jsx-runtime"; + export const unstable_settings = { + anchor: 'index' + }; + export default function Index() { + const data = useLoaderData(); + return /*#__PURE__*/_jsx("div", { + children: data.data + }); + }" + `); }); it('preserves multiple exports in same declaration', () => { @@ -341,23 +348,23 @@ describe('client', () => { expect(res.metadata.performConstantFolding).toBe(true); expect(res.metadata.loaderReference).toBe('/app/index'); expect(res.code).toMatchInlineSnapshot(` - "import { useLoaderData } from 'expo-router'; - import { jsx as _jsx } from "react/jsx-runtime"; - export const unstable_settings = { - anchor: 'index' - }, - generateStaticParams = () => [{ - id: '1' - }, { - id: '2' - }]; - export default function Index() { - const data = useLoaderData(); - return /*#__PURE__*/_jsx("div", { - children: data.data - }); - }" - `); + "import { useLoaderData } from 'expo-router'; + import { jsx as _jsx } from "react/jsx-runtime"; + export const unstable_settings = { + anchor: 'index' + }, + generateStaticParams = () => [{ + id: '1' + }, { + id: '2' + }]; + export default function Index() { + const data = useLoaderData(); + return /*#__PURE__*/_jsx("div", { + children: data.data + }); + }" + `); }); }); @@ -375,13 +382,13 @@ describe('client', () => { expect(res.metadata.performConstantFolding).toBeUndefined(); expect(res.metadata.loaderReference).toBeUndefined(); expect(res.code).toMatchInlineSnapshot(` - "import { jsx as _jsx } from "react/jsx-runtime"; - export default function Index() { - return /*#__PURE__*/_jsx("div", { - children: "Index" - }); - }" - `); + "import { jsx as _jsx } from "react/jsx-runtime"; + export default function Index() { + return /*#__PURE__*/_jsx("div", { + children: "Index" + }); + }" + `); }); it('handles files with only loader export', () => { @@ -424,67 +431,29 @@ describe('client', () => { expect(res.metadata.performConstantFolding).toBeUndefined(); expect(res.code).toMatchInlineSnapshot(` - "import { jsx as _jsx } from "react/jsx-runtime"; - export function loader() { - return { - data: 'test' - }; - } - function noop() { - return; - } - export function MyComponent() { - noop(); - const data = loader(); - return /*#__PURE__*/_jsx("div", { - children: data.data - }); - }" - `); + "import { jsx as _jsx } from "react/jsx-runtime"; + export function loader() { + return { + data: 'test' + }; + } + function noop() { + return; + } + export function MyComponent() { + noop(); + const data = loader(); + return /*#__PURE__*/_jsx("div", { + children: data.data + }); + }" + `); }); }); }); -// NOTE(@hassankhan): Server bundles preserve loaders for SSG. A followup is required to strip -// loaders from server bundles. describe('server', () => { describe('preserves exports', () => { - it('preserves loader exports', () => { - const res = transformTest( - ` - import { useLoaderData } from 'expo-router'; - - export async function loader() { - return { data: 'test' }; - } - - export default function Index() { - const data = useLoaderData(); - return
{data.data}
; - } - `, - { bundleType: 'server' } - ); - - expect(res.metadata.performConstantFolding).toBeUndefined(); - expect(res.metadata.loaderReference).toBeUndefined(); - expect(res.code).toMatchInlineSnapshot(` - "import { useLoaderData } from 'expo-router'; - import { jsx as _jsx } from "react/jsx-runtime"; - export async function loader() { - return { - data: 'test' - }; - } - export default function Index() { - const data = useLoaderData(); - return /*#__PURE__*/_jsx("div", { - children: data.data - }); - }" - `); - }); - it('preserves non-loader exports', () => { const res = transformTest( ` @@ -501,23 +470,19 @@ describe('server', () => { { bundleType: 'server' } ); - expect(res.metadata.performConstantFolding).toBeUndefined(); + expect(res.metadata.performConstantFolding).toBe(true); + expect(res.metadata.loaderReference).toBe('/app/index'); expect(res.code).toMatchInlineSnapshot(` - "import { jsx as _jsx } from "react/jsx-runtime"; - export async function loader() { - return { - data: 'test' + "import { jsx as _jsx } from "react/jsx-runtime"; + export const unstable_settings = { + anchor: 'index' }; - } - export const unstable_settings = { - anchor: 'index' - }; - export default function Index() { - return /*#__PURE__*/_jsx("div", { - children: "Index" - }); - }" - `); + export default function Index() { + return /*#__PURE__*/_jsx("div", { + children: "Index" + }); + }" + `); }); }); }); @@ -544,13 +509,13 @@ describe('loader', () => { expect(res.metadata.performConstantFolding).toBe(true); expect(res.metadata.loaderReference).toBe('/app/index'); expect(res.code).toMatchInlineSnapshot(` - "import { useLoaderData } from 'expo-router'; - export async function loader() { - return { - data: 'test' - }; - }" - `); + "import { useLoaderData } from 'expo-router'; + export async function loader() { + return { + data: 'test' + }; + }" + `); }); it('removes other named exports in loader bundles', () => { @@ -576,12 +541,12 @@ describe('loader', () => { expect(res.metadata.performConstantFolding).toBe(true); expect(res.metadata.loaderReference).toBe('/app/index'); expect(res.code).toMatchInlineSnapshot(` - "export async function loader() { - return { - data: 'test' - }; - }" - `); + "export async function loader() { + return { + data: 'test' + }; + }" + `); }); }); @@ -603,12 +568,12 @@ describe('loader', () => { expect(res.metadata.performConstantFolding).toBe(true); expect(res.metadata.loaderReference).toBe('/app/index'); expect(res.code).toMatchInlineSnapshot(` - "export function loader() { - return { - data: 'test' - }; - }" - `); + "export function loader() { + return { + data: 'test' + }; + }" + `); }); it('preserves loader const arrow function', () => { @@ -628,12 +593,12 @@ describe('loader', () => { expect(res.metadata.performConstantFolding).toBe(true); expect(res.metadata.loaderReference).toBe('/app/index'); expect(res.code).toMatchInlineSnapshot(` - "export const loader = async () => { - return { - data: 'test' - }; - };" - `); + "export const loader = async () => { + return { + data: 'test' + }; + };" + `); }); it('preserves loader const function expression', () => { @@ -653,12 +618,12 @@ describe('loader', () => { expect(res.metadata.performConstantFolding).toBe(true); expect(res.metadata.loaderReference).toBe('/app/index'); expect(res.code).toMatchInlineSnapshot(` - "export const loader = function () { - return { - data: 'test' - }; - };" - `); + "export const loader = function () { + return { + data: 'test' + }; + };" + `); }); it('extracts loader from multi-declaration export', () => { @@ -682,12 +647,12 @@ describe('loader', () => { expect(res.metadata.performConstantFolding).toBe(true); expect(res.metadata.loaderReference).toBe('/app/index'); expect(res.code).toMatchInlineSnapshot(` - "export const loader = async () => { - return { - data: 'test' - }; - };" - `); + "export const loader = async () => { + return { + data: 'test' + }; + };" + `); }); it('preserves loader and its dependencies', () => { @@ -716,22 +681,22 @@ describe('loader', () => { expect(res.metadata.performConstantFolding).toBe(true); expect(res.metadata.loaderReference).toBe('/app/index'); expect(res.code).toMatchInlineSnapshot(` - "import { fetchData } from './api'; - const CACHE_TTL = 3600; - async function getData(id) { - return fetchData(id, { - ttl: CACHE_TTL - }); - } - export async function loader({ - params - }) { - const data = await getData(params.id); - return { - data - }; - }" - `); + "import { fetchData } from './api'; + const CACHE_TTL = 3600; + async function getData(id) { + return fetchData(id, { + ttl: CACHE_TTL + }); + } + export async function loader({ + params + }) { + const data = await getData(params.id); + return { + data + }; + }" + `); }); }); @@ -771,18 +736,18 @@ describe('loader', () => { expect(res.metadata.performConstantFolding).toBeUndefined(); expect(res.metadata.loaderReference).toBeUndefined(); expect(res.code).toMatchInlineSnapshot(` - "import { jsx as _jsx } from "react/jsx-runtime"; - export function loader() { - return { - data: 'test' - }; - } - export default function MyComponent() { - return /*#__PURE__*/_jsx("div", { - children: "Component" - }); - }" - `); + "import { jsx as _jsx } from "react/jsx-runtime"; + export function loader() { + return { + data: 'test' + }; + } + export default function MyComponent() { + return /*#__PURE__*/_jsx("div", { + children: "Component" + }); + }" + `); }); }); }); diff --git a/packages/babel-preset-expo/src/server-data-loaders-plugin.ts b/packages/babel-preset-expo/src/server-data-loaders-plugin.ts index 617513d8bcf1af..7ed631f5c8442c 100644 --- a/packages/babel-preset-expo/src/server-data-loaders-plugin.ts +++ b/packages/babel-preset-expo/src/server-data-loaders-plugin.ts @@ -21,7 +21,6 @@ export function serverDataLoadersPlugin(api: ConfigAPI & typeof import('@babel/c const { types: t } = api; const routerAbsoluteRoot = api.caller(getExpoRouterAbsoluteAppRoot); - const isServer = api.caller(getIsServer); const isLoaderBundle = api.caller(getIsLoaderBundle); return { @@ -44,12 +43,6 @@ export function serverDataLoadersPlugin(api: ConfigAPI & typeof import('@babel/c }, ExportNamedDeclaration(path, state) { - // NOTE(@hassankhan): Server bundles currently preserve loaders for SSG, a followup is - // required to remove them. - if (isServer && !isLoaderBundle) { - return; - } - // Early exit if file is not within the `app/` directory if (!isInAppDirectory(state.file.opts.filename ?? '', routerAbsoluteRoot)) { debug('Skipping file outside app directory:', state.file.opts.filename); diff --git a/packages/expo-modules-core/android/src/main/java/expo/modules/core/utilities/KotlinUtilities.kt b/packages/expo-modules-core/android/src/main/java/expo/modules/core/utilities/KotlinUtilities.kt index cdc33bf1d2da10..223a1d86071c7d 100644 --- a/packages/expo-modules-core/android/src/main/java/expo/modules/core/utilities/KotlinUtilities.kt +++ b/packages/expo-modules-core/android/src/main/java/expo/modules/core/utilities/KotlinUtilities.kt @@ -10,14 +10,3 @@ package expo.modules.core.utilities * ``` */ inline fun T?.ifNull(block: () -> T): T = this ?: block() - -/** - * If the receiver is instance of `T`, returns the receiver, otherwise returns `null` - * - * Works the same as the `as?` operator, but allows method chaining without parentheses: - * ``` - * val x = a.b.takeIfInstanceOf?.someMethod() - * val y = (a.b as? Number)?.someMethod() // same, but needs parenthesis - * ``` - */ -inline fun Any?.takeIfInstanceOf(): T? = this as? T diff --git a/packages/expo-notifications/CHANGELOG.md b/packages/expo-notifications/CHANGELOG.md index 4251b2e92692ed..bd1b31ef22d7fb 100644 --- a/packages/expo-notifications/CHANGELOG.md +++ b/packages/expo-notifications/CHANGELOG.md @@ -10,6 +10,8 @@ ### 🐛 Bug fixes +- [Android] fix background task not consistently executing ([#43245](https://github.com/expo/expo/pull/43245) by [@vonovak](https://github.com/vonovak)) +- [Android] fix `deleteNotificationChannelGroupAsync` export ([#43244](https://github.com/expo/expo/pull/43244) by [@vonovak](https://github.com/vonovak)) - add FCM intent origin validation ([#43206](https://github.com/expo/expo/pull/43206) by [@vonovak](https://github.com/vonovak)) - Fixed crash in `NotificationForwarderActivity` on Android 11/12 when Parcelable extras fail to deserialize by using byte array serialization as fallback. ([#43203](https://github.com/expo/expo/pull/43203) by [@vonovak](https://github.com/vonovak)) diff --git a/packages/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/background/BackgroundRemoteNotificationTaskConsumer.java b/packages/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/background/BackgroundRemoteNotificationTaskConsumer.java index 07807aa022ad10..89d1b3f538d618 100644 --- a/packages/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/background/BackgroundRemoteNotificationTaskConsumer.java +++ b/packages/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/background/BackgroundRemoteNotificationTaskConsumer.java @@ -53,6 +53,7 @@ public void didRegister(TaskInterface task) { @Override public void didUnregister() { + FirebaseMessagingDelegate.Companion.removeBackgroundTaskConsumer(this); mTask = null; } diff --git a/packages/expo-notifications/android/src/main/java/expo/modules/notifications/service/delegates/ExpoHandlingDelegate.kt b/packages/expo-notifications/android/src/main/java/expo/modules/notifications/service/delegates/ExpoHandlingDelegate.kt index 23a8838357f71e..668f40b7ce2457 100644 --- a/packages/expo-notifications/android/src/main/java/expo/modules/notifications/service/delegates/ExpoHandlingDelegate.kt +++ b/packages/expo-notifications/android/src/main/java/expo/modules/notifications/service/delegates/ExpoHandlingDelegate.kt @@ -13,7 +13,6 @@ import expo.modules.notifications.notifications.model.Notification import expo.modules.notifications.notifications.model.NotificationResponse import expo.modules.notifications.service.NotificationForwarderActivity import expo.modules.notifications.service.NotificationsService -import expo.modules.notifications.service.delegates.FirebaseMessagingDelegate.Companion.runTaskManagerTasks import expo.modules.notifications.service.interfaces.HandlingDelegate import java.lang.ref.WeakReference import java.util.* @@ -138,14 +137,22 @@ class ExpoHandlingDelegate(protected val context: Context) : HandlingDelegate { } override fun handleNotificationResponse(notificationResponse: NotificationResponse) { - if (!isAppInForeground()) { - // do not run in foreground for better alignment with iOS - // iOS doesn't run background tasks for notification responses at all - runTaskManagerTasks(context.applicationContext, NotificationSerializer.toBundle(notificationResponse)) - } if (notificationResponse.action.opensAppToForeground()) { openAppToForeground(context, notificationResponse) } + + // Run background tasks only for custom notification action buttons (not the default tap). + // When the default notification tap launches the app from killed state, calling + // runTaskManagerTasks starts a headless React instance that races with the foreground app. + // The foreground TaskManager gets misclassified as headless (via isStartedByHeadlessLoader), + // then wiped by invalidateAppRecord — breaking all subsequent background task execution. + if (!isAppInForeground() && notificationResponse.actionIdentifier != NotificationResponse.DEFAULT_ACTION_IDENTIFIER) { + FirebaseMessagingDelegate.runTaskManagerTasks( + context.applicationContext, + NotificationSerializer.toBundle(notificationResponse) + ) + } + // NOTE the listeners are not set up when the app is killed // and is launched in response to tapping a notification button // this code is a noop in that case diff --git a/packages/expo-notifications/android/src/main/java/expo/modules/notifications/service/delegates/FirebaseMessagingDelegate.kt b/packages/expo-notifications/android/src/main/java/expo/modules/notifications/service/delegates/FirebaseMessagingDelegate.kt index 5316555e703458..eecdae82e99d29 100644 --- a/packages/expo-notifications/android/src/main/java/expo/modules/notifications/service/delegates/FirebaseMessagingDelegate.kt +++ b/packages/expo-notifications/android/src/main/java/expo/modules/notifications/service/delegates/FirebaseMessagingDelegate.kt @@ -15,7 +15,6 @@ import expo.modules.notifications.notifications.model.triggers.FirebaseNotificat import expo.modules.notifications.service.NotificationsService import expo.modules.notifications.service.interfaces.FirebaseMessagingDelegate import expo.modules.notifications.tokens.interfaces.FirebaseTokenListener -import java.lang.ref.WeakReference import java.util.* open class FirebaseMessagingDelegate(protected val context: Context) : FirebaseMessagingDelegate { @@ -62,26 +61,25 @@ open class FirebaseMessagingDelegate(protected val context: Context) : FirebaseM } /** - * TODO vonovak this WeakHashMap is an awkward construct - simplify it - * A weak map of task consumers -> reference. Used to check quickly whether given task - * is already registered and to iterate over when notifying of new notification received + * A set of background task consumers, notified when a notification is received * while the app is not in the foreground. */ - protected var sBackgroundTaskConsumerReferences = WeakHashMap>() + protected var sBackgroundTaskConsumers = mutableSetOf() /** * Background tasks are registered in [BackgroundRemoteNotificationTaskConsumer] instances. * - * @param taskConsumer A task instance to be executed when a notification is received while the * app is not in the foreground + * @param taskConsumer A task instance to be executed when a notification is received while the app is not in the foreground */ fun addBackgroundTaskConsumer(taskConsumer: BackgroundRemoteNotificationTaskConsumer) { - if (sBackgroundTaskConsumerReferences.containsKey(taskConsumer)) { - return - } - sBackgroundTaskConsumerReferences[taskConsumer] = WeakReference(taskConsumer) + sBackgroundTaskConsumers.add(taskConsumer) + } + + fun removeBackgroundTaskConsumer(taskConsumer: BackgroundRemoteNotificationTaskConsumer) { + sBackgroundTaskConsumers.remove(taskConsumer) } - fun getBackgroundTasks() = sBackgroundTaskConsumerReferences.values.mapNotNull { it.get() } + fun getBackgroundTasks(): List = sBackgroundTaskConsumers.toList() fun runTaskManagerTasks(applicationContext: Context, bundle: Bundle) { // getTaskServiceImpl() has a side effect: diff --git a/packages/expo-notifications/build/deleteNotificationChannelGroupAsync.android.d.ts b/packages/expo-notifications/build/deleteNotificationChannelGroupAsync.android.d.ts index 01749ed312ceae..ae6ec4314bfec2 100644 --- a/packages/expo-notifications/build/deleteNotificationChannelGroupAsync.android.d.ts +++ b/packages/expo-notifications/build/deleteNotificationChannelGroupAsync.android.d.ts @@ -1,2 +1,2 @@ -export declare function deleteNotificationChannelAsync(groupId: string): Promise; +export declare function deleteNotificationChannelGroupAsync(groupId: string): Promise; //# sourceMappingURL=deleteNotificationChannelGroupAsync.android.d.ts.map \ No newline at end of file diff --git a/packages/expo-notifications/build/deleteNotificationChannelGroupAsync.android.d.ts.map b/packages/expo-notifications/build/deleteNotificationChannelGroupAsync.android.d.ts.map index cba436249a5e70..b9eb2390cf46eb 100644 --- a/packages/expo-notifications/build/deleteNotificationChannelGroupAsync.android.d.ts.map +++ b/packages/expo-notifications/build/deleteNotificationChannelGroupAsync.android.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"deleteNotificationChannelGroupAsync.android.d.ts","sourceRoot":"","sources":["../src/deleteNotificationChannelGroupAsync.android.ts"],"names":[],"mappings":"AAIA,wBAAsB,8BAA8B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAMnF"} \ No newline at end of file +{"version":3,"file":"deleteNotificationChannelGroupAsync.android.d.ts","sourceRoot":"","sources":["../src/deleteNotificationChannelGroupAsync.android.ts"],"names":[],"mappings":"AAIA,wBAAsB,mCAAmC,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAMxF"} \ No newline at end of file diff --git a/packages/expo-notifications/build/deleteNotificationChannelGroupAsync.android.js b/packages/expo-notifications/build/deleteNotificationChannelGroupAsync.android.js index c804c2ccab1037..0612cab0ddc766 100644 --- a/packages/expo-notifications/build/deleteNotificationChannelGroupAsync.android.js +++ b/packages/expo-notifications/build/deleteNotificationChannelGroupAsync.android.js @@ -1,6 +1,6 @@ import { UnavailabilityError } from 'expo-modules-core'; import NotificationChannelGroupManager from './NotificationChannelGroupManager'; -export async function deleteNotificationChannelAsync(groupId) { +export async function deleteNotificationChannelGroupAsync(groupId) { if (!NotificationChannelGroupManager.deleteNotificationChannelGroupAsync) { throw new UnavailabilityError('Notifications', 'deleteNotificationChannelGroupAsync'); } diff --git a/packages/expo-notifications/build/deleteNotificationChannelGroupAsync.android.js.map b/packages/expo-notifications/build/deleteNotificationChannelGroupAsync.android.js.map index 0ae9feb1878b0e..25505c4471ecb5 100644 --- a/packages/expo-notifications/build/deleteNotificationChannelGroupAsync.android.js.map +++ b/packages/expo-notifications/build/deleteNotificationChannelGroupAsync.android.js.map @@ -1 +1 @@ -{"version":3,"file":"deleteNotificationChannelGroupAsync.android.js","sourceRoot":"","sources":["../src/deleteNotificationChannelGroupAsync.android.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAExD,OAAO,+BAA+B,MAAM,mCAAmC,CAAC;AAEhF,MAAM,CAAC,KAAK,UAAU,8BAA8B,CAAC,OAAe;IAClE,IAAI,CAAC,+BAA+B,CAAC,mCAAmC,EAAE,CAAC;QACzE,MAAM,IAAI,mBAAmB,CAAC,eAAe,EAAE,qCAAqC,CAAC,CAAC;IACxF,CAAC;IAED,OAAO,MAAM,+BAA+B,CAAC,mCAAmC,CAAC,OAAO,CAAC,CAAC;AAC5F,CAAC","sourcesContent":["import { UnavailabilityError } from 'expo-modules-core';\n\nimport NotificationChannelGroupManager from './NotificationChannelGroupManager';\n\nexport async function deleteNotificationChannelAsync(groupId: string): Promise {\n if (!NotificationChannelGroupManager.deleteNotificationChannelGroupAsync) {\n throw new UnavailabilityError('Notifications', 'deleteNotificationChannelGroupAsync');\n }\n\n return await NotificationChannelGroupManager.deleteNotificationChannelGroupAsync(groupId);\n}\n"]} \ No newline at end of file +{"version":3,"file":"deleteNotificationChannelGroupAsync.android.js","sourceRoot":"","sources":["../src/deleteNotificationChannelGroupAsync.android.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAExD,OAAO,+BAA+B,MAAM,mCAAmC,CAAC;AAEhF,MAAM,CAAC,KAAK,UAAU,mCAAmC,CAAC,OAAe;IACvE,IAAI,CAAC,+BAA+B,CAAC,mCAAmC,EAAE,CAAC;QACzE,MAAM,IAAI,mBAAmB,CAAC,eAAe,EAAE,qCAAqC,CAAC,CAAC;IACxF,CAAC;IAED,OAAO,MAAM,+BAA+B,CAAC,mCAAmC,CAAC,OAAO,CAAC,CAAC;AAC5F,CAAC","sourcesContent":["import { UnavailabilityError } from 'expo-modules-core';\n\nimport NotificationChannelGroupManager from './NotificationChannelGroupManager';\n\nexport async function deleteNotificationChannelGroupAsync(groupId: string): Promise {\n if (!NotificationChannelGroupManager.deleteNotificationChannelGroupAsync) {\n throw new UnavailabilityError('Notifications', 'deleteNotificationChannelGroupAsync');\n }\n\n return await NotificationChannelGroupManager.deleteNotificationChannelGroupAsync(groupId);\n}\n"]} \ No newline at end of file diff --git a/packages/expo-notifications/src/deleteNotificationChannelGroupAsync.android.ts b/packages/expo-notifications/src/deleteNotificationChannelGroupAsync.android.ts index 874139b1b6f880..7d004d6ac5b97d 100644 --- a/packages/expo-notifications/src/deleteNotificationChannelGroupAsync.android.ts +++ b/packages/expo-notifications/src/deleteNotificationChannelGroupAsync.android.ts @@ -2,7 +2,7 @@ import { UnavailabilityError } from 'expo-modules-core'; import NotificationChannelGroupManager from './NotificationChannelGroupManager'; -export async function deleteNotificationChannelAsync(groupId: string): Promise { +export async function deleteNotificationChannelGroupAsync(groupId: string): Promise { if (!NotificationChannelGroupManager.deleteNotificationChannelGroupAsync) { throw new UnavailabilityError('Notifications', 'deleteNotificationChannelGroupAsync'); } diff --git a/packages/expo-task-manager/android/src/main/java/expo/modules/taskManager/TaskService.java b/packages/expo-task-manager/android/src/main/java/expo/modules/taskManager/TaskService.java index 97a662b206de63..124ba1ead6777a 100644 --- a/packages/expo-task-manager/android/src/main/java/expo/modules/taskManager/TaskService.java +++ b/packages/expo-task-manager/android/src/main/java/expo/modules/taskManager/TaskService.java @@ -366,7 +366,8 @@ public boolean cancelJob(JobService jobService, JobParameters params) { } public void executeTask(TaskInterface task, Bundle data, Error error, TaskExecutionCallback callback) { - TaskManagerInterface taskManager = getTaskManager(task.getAppScopeKey()); + String appScopeKey = task.getAppScopeKey(); + TaskManagerInterface taskManager = getTaskManager(appScopeKey); Bundle body = createExecutionEventBody(task, data, error); Bundle executionInfo = body.getBundle("executionInfo"); @@ -376,7 +377,6 @@ public void executeTask(TaskInterface task, Bundle data, Error error, TaskExecut } String eventId = executionInfo.getString("eventId"); - String appScopeKey = task.getAppScopeKey(); if (callback != null) { sTaskCallbacks.put(eventId, callback);