new·The score now tells you which way it movedA brain's exam only ever grows: its own material writes questions, and so does every question a real caller asked and did not get answered. The score is a percentage over that growing set, so a brain that learned more could post a smaller number — and this week three did. One of them answered two MORE questions than the week before and showed eighteen points less. Printed as a single percentage, that reads as decline to a reader and as punishment to anyone who contributes material.all news →
mozg.beta
Sign in

Expo & React Native · all subjects

config-plugins & native code

121 notes in this subject, read out of this brain and free to use. This is page 1 of 3.

Updating Expo config: Prebuild config test fixture

Add the new property to `getLargeConfig()` test fixture in `packages/@expo/prebuild-config/src/plugins/__tests__/withDefaultPlugins-test.ts`.

Updating Expo config: Build and test commands

After making changes, run these build and verification commands in order: (1) cd packages/@expo/config-types && pnpm build; (2) cd packages/@expo/config-plugins && pnpm build && pnpm typecheck && pnpm test src/ios/__tests__/PropertyName-test.ts; (3) cd packages/@expo/prebuild-config && pnpm build && pnpm typecheck

Updating Expo config: Docs schema auto-generation

The docs schema file is auto-generated from the universe schema via the Expo API server. After the universe schema is deployed, synchronize the docs schema by running: cd docs && pnpm run schema-sync unversioned

Updating Expo config: JSON schema file location and structure

When adding a new property to the Expo config schema, update the JSON schema first in the file `../universe/server/www/xdl-schemas/UNVERSIONED-schema.json`. Each property must include: description (human-readable description), type (JSON schema type), pattern (regex validation if applicable), and meta.bareWorkflow (instructions for bare workflow users). Example structure: { "propertyName": { "description": "Description of the property.", "type": "string", "pattern": "^\\d+\\.\\d+$", "meta": { "bareWorkflow": "Instructions for bare workflow" } } }. This step only works for Expo team members with access to the server repository.

Updating Expo config: Complete checklist of steps

When adding a new property to Expo config, follow this complete checklist: (1) JSON schema in universe (UNVERSIONED-schema.json), (2) TypeScript types in @expo/config-types, (3) Docs schema (run cd docs && yarn run schema-sync unversioned after universe deploy, or manually update for local dev), (4) Config plugin if needed, (5) Tests for plugin.

Updating Expo config: Config plugin test file structure

Create config plugin tests in file `packages/@expo/config-plugins/src/ios/__tests__/PropertyName-test.ts`. The test should use describe blocks for the module and getter function, with test cases checking that the getter returns null if not set and returns the value when provided.

Updating Expo config: TypeScript types generation command

After updating the JSON schema in universe, generate TypeScript types by running: cd packages/@expo/config-types && pnpm generate --path ../../../../universe/server/www/xdl-schemas/UNVERSIONED-schema.json

Updating Expo config: Config plugin creation and registration

If a new config property needs to be applied during prebuild, create a new plugin module following existing patterns, generally in `packages/@expo/config-plugins/src`. Ensure the plugin is registered for prebuild in `packages/@expo/prebuild-config`.

Expo Client iOS dependency management with CocoaPods

Dependencies are managed via CocoaPods using a template file at template-files/ios/dependencies.json rather than committing the Podfile directly. To add a dependency, add it to dependencies.json, then run et ios-generate-dynamic-macros to generate the Podfile for Expo Client and ExpoKit.podspec for standalone apps.

Versioned directory contents in Expo iOS

The versioned directory contains code that is duplicated and namespaced when releasing the next SDK version. It includes all Expo SDK native modules, components, and view managers (the unversioned Expo SDK), utility classes for running namespaced and scoped modules, and older versions appear under versioned-react-native/ABI*/Exponent.

Full-resolution bitmap decode requires downsampling

Full-resolution JPEG or image decode with no downsample against target bounds consumes excessive memory. A 12 MP JPEG becomes ~48 MB ARGB_8888 regardless of display size. Use BitmapFactory.Options.inSampleSize or hand decoding to Coil/Glide.

Stale state after lifecycle transitions invalidates cached measurements

Cached sizes, layouts, or measurements must be invalidated when lifecycle or configuration transitions occur, such as keyboard dismiss, screen rotation, or activity recreation. Failure to invalidate stale cached state leads to incorrect rendering and logic errors.

Index and offset arithmetic invalid by concurrent collection edits

Index and offset arithmetic over a collection can be invalidated by concurrent edits from other threads. This causes crashes or logic errors when the collection is modified while iteration is in progress.

Reflection and JNI access require ProGuard keep rules

Any class, field, or method reached by reflection, JNI, or name lookup must have a matching keep rule in proguard-rules.pro or consumer-rules.pro. This includes Record subclasses, enum valueOf, Gson/Moshi models, and anything named from C++.

Glide target sizing must use Target.SIZE_ORIGINAL sentinel, not Int.MAX_VALUE

Glide defines Target.SIZE_ORIGINAL as the canonical no-bound sentinel on an axis. Using Int.MAX_VALUE happens to work only because centerInside() caps the scale and breaks if downsample strategy changes.

Conditional mutations with guards that silently skip are logic defects

A conditional mutation guarded by indexOf returning -1, a nullable map lookup, or a ?.let { } block where the null branch does nothing can leave an invariant unstated by the code. These must be reviewed for whether the skip preserves correctness.

targetSdk-triggered behavior must be consistent across all paths

New targetSdk-triggered behavior such as background restrictions, exact alarms, foreground service types, predictive back, and PendingIntent mutability must be adopted consistently across all code paths, not just one.

Long-lived listeners must be removed in matching lifecycle

A long-lived or process-global listener registered from a module (registerContentObserver, registerReceiver, sensor/location listeners, lambda in companion object collection, MediaPlayer/ExoPlayer callbacks) must have a matching removal in the same diff.

API capability checks must gate both the check and the use

When code gates a capability check with an SDK level guard, the use of that capability must also be gated at the same level. A code path reachable below the guard must not call the newer API.

GlobalScope.launch and fresh CoroutineScope must be cancelled on lifecycle

GlobalScope.launch or a newly constructed CoroutineScope held by a module, view, or shared object must have matching cancel() in OnDestroy, OnViewDestroys, or sharedObjectDidRelease. A scope must be deliberately tied to process or release/reload lifetime, not module lifetime.

Int used for file/media/buffer size causes silent wraparound above 2 GB

Android APIs return Long for file, media, and buffer sizes. Using Int instead causes silent wraparound past 2 GB when narrowing conversions occur via .toInt() calls or Int parameters. Check all .toInt() calls, Int parameters, and arithmetic that multiplies dimensions for file or media size handling.

Cache and lock file names must not collide across instances or restarts

Cache, lock file, and directory names must be unique to avoid collisions between concurrent instances or after process restart.

Collection mutations during teardown cause ConcurrentModificationException

A collection mutated during teardown or reload while another thread iterates it causes ConcurrentModificationException. Use thread-safe collections or synchronization.

Shared OkHttpClient must be reused, not built per-module

An OkHttpClient must not be built per-module with no shared cache. The repo routes through a shared client to avoid cache duplication and connection pooling overhead.

Validation order must be consistent across sibling methods

When multiple methods of one class call validation steps, they must do so in the same order. If bytes() and asContentUri() validate type before permission, a new digest() method must follow the same order or risk throwing raw exceptions instead of the class's documented exception types.

Downsample strategy must not contradict the cap it enforces

SampleSizeRounding.QUALITY rounds toward a larger bitmap and can exceed hardware limits. Choose a rounding strategy that enforces the capacity guarantee the strategy was written to provide.

SDK level gating: API levels 24 to 36 within minSdk/targetSdk range

The repo's minSdk is 24 and targetSdk is 36. All platform APIs must be guarded with Build.VERSION.SDK_INT checks at the correct API level. Both availability changes and behavior changes across API levels must be guarded appropriately.

sendEvent name must be declared in Events(...) DSL

Every sendEvent() call must use a name that is declared in that module's Events(...) DSL block. Removing a name from Events(...) while sendEvent calls remain is also a defect.

Resume and restore paths must not unconditionally restart all work

An onHostResume handler must not unconditionally restart every watcher or subscription, including ones the caller never subscribed to. Restart paths need the same initialization guard that prevents duplicate startup as other code paths.

Record properties require @Field annotation with matching key

New or renamed properties on a Kotlin Record must have a @Field annotation. The @Field(key = ...) must match the key that the TypeScript side sends.

Blocking work must not be in Function(...) body

Do not put blocking work inside a synchronous Function(...) body. File I/O, network I/O, ContentResolver queries, Thread.sleep, runBlocking, CountDownLatch.await, bitmap decode, and database access must move to AsyncFunction or AsyncFunction … Coroutine.

File paths derived from caller input require traversal checks

Any File path or filename derived from caller-supplied input must include traversal validation to prevent directory traversal attacks.

Failure paths must emit signals to JavaScript

Unresolvable asset IDs, unsupported schemes, HTTP errors, and decode failures must not produce blank space with only a logcat line. Expose onLoad/onError via ViewEvent pattern or document the behavior.

Error messages must state what, why, and how failed

Error messages must not state only what failed. Follow repo guidance: include what failed, why it failed, and how to fix it.

Thrown exceptions must be CodedException, not generic types

Do not throw raw IllegalArgumentException, IllegalStateException, or bare Exception where JavaScript code branches on the failure. expo-modules-core wraps those as ERR_UNEXPECTED. Add a CodedException subclass instead.

Long-running blocking work in AsyncFunction must hop off default queue

Long-running blocking work in an AsyncFunction must hop off the default queue with withContext(Dispatchers.IO) in a Coroutine body or .runOnQueue(appContext.backgroundCoroutineScope). Inside a View { … } block always hop off, including cleanup like response.close().

JSI wrappers must be routed through runtime.schedule from background context

JavaScriptObject, JavaScriptValue, JavaScriptFunction, JavaScriptWeakObject, and ArrayBuffer unscoped accessors must be touched from background context only by routing through runtime.schedule { }.

Concurrent AsyncFunction invocation must handle multiple in-flight calls

AsyncFunction implementations that assume only one call in flight are not thread-safe. A shared prompt, single callback slot, or non-reentrant SDK exposed to concurrent invocation causes the second caller to see incorrect state.

Record and ComposeProps constructor parameters must have defaults

Every primary-constructor parameter on a Kotlin Record or ComposeProps must have a default value. Constructor parameters added without defaults break serialization.

ContentResolver queries must use selectionArgs not concatenation

ContentResolver selection strings must be built using selectionArgs parameters, not string concatenation. Identifier lists must be escaped. Raw SQL strings must not be assembled from caller-supplied values.

Activity, Context, View, and AppContext must use WeakReference in singletons

An Activity, ReactContext, View, or AppContext stored strongly in a companion object, top-level object, or any singleton causes leaks and crashes. Always use WeakReference or .weak().

AsyncFunction Promise must be settled on every path

An AsyncFunction taking a Promise must settle it (resolve or reject) on every code path. An early return, guard, catch, or callback branch that leaves the promise neither resolved nor rejected is a defect.

Use SceneGeometry for screen geometry, not process-global APIs

Screen geometry must not be read from process-global APIs like UIScreen.main.bounds, UIScreen.main.scale, UIApplication.shared.windows.first, or UIWindowScene.interfaceOrientation, or cached in a static or stored property. Use SceneGeometry.bounds(for:), displayScale(for:), or keyWindow(for:) instead, because scale differs per scene and must never be cached.

PhotoKit resource selection by index not mediaType is unsafe

PhotoKit resource selection by array index rather than by mediaType is unsafe. Apple does not guarantee resource ordering, and resources.first can return a .pairedVideo on a Live Photo.

UIActivityViewController completes on dismissal, not consumer finish

UIActivityViewController.completionWithItemsHandler is a completion handler that fires on dismissal rather than on the consumer finishing. Mail, Files, and AirDrop keep reading the source URL after the sheet closes, so deleting a staged file in that handler truncates the share.

AVQueuePlayer advances forward only

AVQueuePlayer advances forward only. An item played to its end will not replay if re-inserted, so an edit at or before the current index requires rebuilding the queue rather than splicing.

Research framework semantics before asserting

Before reporting a semantics-based finding about Apple frameworks, research the API. Available sources in order of strength are: React Native's own source vendored in-tree (react-native-lab/react-native/packages/react-native/), sibling Expo packages already calling the same API, expo-modules-core's own DSL definitions, and the package's .podspec for deployment targets and platform availability. If none settle it, do not upgrade a guess into a finding.

Stale state when active instance changes

Stale state when the active instance changes is a defect. A registry, now-playing info, or shared controller updated on activate but not cleared on switch leaves the next instance inheriting the previous one's values.

Geometry and layout arithmetic on unrounded or unscaled values

Geometry and layout arithmetic on unrounded or unscaled values is a defect when the result feeds a pixel offset or frame.

Precedence chain returning early on resolvable but unusable value

A precedence chain that returns early on a value that resolves but is not usable is a defect. UTType("public.image") resolves fine and has no preferredFilenameExtension, so a perfectly good mimeType fallback never runs. Select on usability, not mere resolvability.

Path and filename string surgery is error-prone

deletingPathExtension() strips whatever follows the last dot even when it is not an extension, so Q3 Report.v2 becomes Q3 Report. Prefer appending to lastPathComponent over replacing.

String and byte handling assuming ASCII or fixed width is a defect

String and byte handling that assumes ASCII, single-byte characters, or a fixed width is a defect. Using count as a byte length, utf8 length as a character count, or a fixed buffer for a name can truncate non-ASCII property keys.

Recursion with no base case or skippable base case is a defect

Recursion with no base case, or a base case an input can skip, in encoders, converters, and type coercion is a defect. For example, infinite recursion encoding Date.

NotificationCenter observations are global, not instance-scoped

NotificationCenter observations are global, not scoped to the instance that registered them. Code assuming a notification concerns my player or my view needs to check the object.

Limit, count, or range parameter treated as unset when zero or negative

A limit, count, or range parameter with a meaningful zero or negative value treated as unset is a defect. For example, limit(0) returning every asset when it should return nothing.

Index and offset arithmetic over changing collection length is a defect

Index and offset arithmetic over a collection whose length can change between computation and use is a defect. A cached index into a playlist, queue, or cursor that a concurrent edit invalidates is an example.

Conditional mutation guard that silently skips is a defect

A conditional mutation whose guard can silently skip it, leaving an invariant the code itself states, is a logic defect. The shape is if let i = xs.firstIndex(of: key) { … } or if let x = dict[k] { … }, or a guard around a reorder, insert, or normalize step where the nil branch does nothing and the surrounding comment or contract says the result must hold. This is a defect, not a style issue, if a comment says the first element has to be X next to code that only sometimes puts X first.

Private-API class or selector as plain string literal

A private-API class or selector written as a plain string literal is flagged by static analysis on App Store submission. Split the literal ("_UI".appending("ContextMenuContainerView")) when the usage is deliberate.

Swift language mode and deployment targets for Expo packages

The correctness-ios reviewer checks against Swift language mode 6.0 for expo-modules-core (5.9 for expo-ui and expo-updates), iOS/tvOS deployment target 16.4, and Xcode 26.4.1 in CI.

Correctness reviewer scope: iOS Swift and Objective-C

The correctness-ios reviewer reviews Swift code under packages/*/ios/ and packages/*/apple/, and Objective-C under the same roots. The reviewer owns both the logic of that code and its use of the Expo Modules API. Kotlin and Java belong to correctness-android. Defects that only appear when iOS and Android disagree belong to the cross-cutting correctness reviewer, reported on the iOS side here.

Give your agent this brain