diff --git a/content/docs/expo-devtools/changelog.mdx b/content/docs/expo-devtools/changelog.mdx index b178cc7..c0deac6 100644 --- a/content/docs/expo-devtools/changelog.mdx +++ b/content/docs/expo-devtools/changelog.mdx @@ -7,6 +7,64 @@ description: Every published release of @axonpack/expo-devtools, newest first. Every published release, newest first. The current version is **2.5.4**. +## 3.0.0 + + + +- The whole list of what moved where is in + [Upgrading](https://axonpack.github.io/docs/expo-devtools/upgrading). + +**⚠️ Breaking Changes** + +- Setting the devtools up is now one provider around your app. `createDevtoolsClient`, `init()` and `` are gone, and there is no client to create or pass anywhere: + + ```tsx + // before + export const devtools = createDevtoolsClient({ defaultTheme: 'dark' }); + if (__DEV__) devtools.init(); + + <> + + {__DEV__ && } + ; + + // after + + + ; + ``` + +- **`enabled` is back, and it is the only gate.** With it off the provider patches nothing, records nothing and draws no button, so the mount can stay in a release build unguarded. It is read on the first render, so it cannot be changed later in the session. + +- **An in-app browser takes one hook.** `useDevtoolsWebView` returns every prop the `` needs, in place of the four client helpers: + + ```tsx + // before + ; + + // after + const devtoolsWebView = useDevtoolsWebView('checkout'); + ; + ``` + +- **`webviewSources` is gone.** A browser view's name is whatever you hand the hook, and it is only the label its rows carry. Drop the option; nothing needs declaring up front. + +- **`mark`, `measure`, `clearMarks`, `clearMeasures`, `setCrashContext` and the stores** now come from the exported `devtools` object: `import { devtools } from '@axonpack/expo-devtools'`. + +**✨ Features** + +- **Open the panel from your own code.** `useDevtoolsPanel()` gives you `show`, `hide`, `toggle`, whether it is open, and whether the devtools are running at all. +- **Hide the floating button.** `showFloatingButton={false}` leaves the panel working and takes the button off your screens. +- **Everything is patched before your first screen mounts,** including whatever it requests as it appears. + ## 2.5.4 diff --git a/content/docs/expo-devtools/console.mdx b/content/docs/expo-devtools/console.mdx index d3d0759..279005a 100644 --- a/content/docs/expo-devtools/console.mdx +++ b/content/docs/expo-devtools/console.mdx @@ -44,20 +44,18 @@ Your app's files are bundled as private closures, so nothing can reach an import way a browser console reaches a page's variables. Anything you want to poke at by name, hand over in `context`: -```ts -createDevtoolsClient({ - console: { context: { store, queryClient } }, -}); +```tsx + ``` It is also the only thing that works in a release build, where the module list the two helpers above read is not available. - `console.repl` defaults to `true`, and it is not gated on `__DEV__`. Once `init()` has run, the - prompt is there — including in a release build, where it will run whatever is typed into it. Guard - your `init()` call (see [Leaving it in production](/docs/expo-devtools/production)), or turn the prompt off - explicitly with `console: { repl: false }`. + `console.repl` defaults to `true`, and it is not gated on `__DEV__`. Wherever the devtools are on the + prompt is there — including in a release build, where it will run whatever is typed into it. Ship + with `enabled: false` (see [Leaving it in production](/docs/expo-devtools/production)), or turn the prompt + off explicitly with `console: { repl: false }`. ## Limits diff --git a/content/docs/expo-devtools/crash-reporting.mdx b/content/docs/expo-devtools/crash-reporting.mdx index a743e48..8bae44a 100644 --- a/content/docs/expo-devtools/crash-reporting.mdx +++ b/content/docs/expo-devtools/crash-reporting.mdx @@ -23,24 +23,25 @@ Which tier caught a crash decides how much it can say. ## Reporting from a release build -Crash capture has the only gate that is not `init()`. Setting one flag installs the handlers when the -client is **constructed**, so an app can keep its usual development-only `init()` call and still report -crashes from release: +Crash capture has the only gate that is not `enabled`. Setting one flag installs the handlers even +with the devtools off, so an app can keep its usual `enabled: __DEV__` and still report crashes from +release: ```ts title="devtools.ts" -export const devtools = createDevtoolsClient({ +export const devtoolsConfig = { + enabled: __DEV__, crash: { enableWhileDevtoolsDisabled: true }, -}); +} satisfies DevtoolsConfig; ``` -On its own that captures **native exceptions only** — the crashes that end the app — and reports them -in the compact sheet. A later `init()` upgrades it: the JS tiers install too and the full sheet takes +With the devtools off that captures **native exceptions only** — the crashes that end the app — and +reports them in the compact sheet. With them on, the JS tiers install too and the full sheet takes over. It brings nothing else with it either way: no panel, no REPL, no console capture, no request bodies. -The JS tiers are held back before `init()` on purpose. They report errors the app survived, which is a -developer's concern, and the sheet there is in front of somebody using the app. A fatal JS error still -arrives, because React Native turns it into a native exception on its way to killing the process. +The JS tiers are held back on purpose. They report errors the app survived, which is a developer's +concern, and the sheet there is in front of somebody using the app. A fatal JS error still arrives, +because React Native turns it into a native exception on its way to killing the process. `popupDetail` defaults to `'auto'`, which picks between two sheets. With the devtools enabled you get @@ -56,8 +57,8 @@ If you ship crash reporting without the panel, mount the sheet yourself: import { CrashReportOverlay } from '@axonpack/expo-devtools'; ``` -`` already mounts one, and mounting both is harmless: whichever mounted first owns -the sheet and the other draws nothing. +`` already mounts one, with the devtools on or off, and mounting both is harmless: +whichever mounted first owns the sheet and the other draws nothing. ## Catching render errors @@ -89,13 +90,14 @@ Everything you pass is attached to every record from that point on. To rewrite or drop a record before it is stored, handed to `onCrash` or written to disk, use `redact`: -```ts -createDevtoolsClient({ - crash: { - redact: (record) => (record.message.includes('token') ? null : record), - onCrash: (record) => myBackend.send(record), - }, -}); +```tsx + (record.message.includes('token') ? null : record), + onCrash: (record) => myBackend.send(record), + }, + }}> ``` ## Decisions worth knowing @@ -128,6 +130,6 @@ createDevtoolsClient({ ## Next step - + diff --git a/content/docs/expo-devtools/debug.mdx b/content/docs/expo-devtools/debug.mdx index 4132bd3..ac618b7 100644 --- a/content/docs/expo-devtools/debug.mdx +++ b/content/docs/expo-devtools/debug.mdx @@ -26,10 +26,10 @@ The two are not the same event, and the difference is worth seeing once. A JS th reported before you let go of the button, while a main-thread crash ends the process and is read back off disk at the next launch. Either way the report is waiting on the Crashes tab. - - They call straight into the native module, so they work whenever the panel is on screen, whether or - not `.init()` ran. Guarding the `` mount is what keeps them out of a release — - see [Leaving it in production](/docs/expo-devtools/production). + + They call straight into the native module, so they work whenever the panel is on screen. + `enabled: false` is what keeps them out of a release, because it is what makes the panel + unreachable — see [Leaving it in production](/docs/expo-devtools/production). There is no record button and nothing to clear, so the tab carries no toolbar. diff --git a/content/docs/expo-devtools/example-app.mdx b/content/docs/expo-devtools/example-app.mdx index 06bd020..a494feb 100644 --- a/content/docs/expo-devtools/example-app.mdx +++ b/content/docs/expo-devtools/example-app.mdx @@ -33,8 +33,8 @@ bun run ios # or: bun run android (full native build) ## A worked configuration `example/devtools.ts` doubles as a worked configuration: a dark default theme, a custom `midnight` one, -two declared `webviewSources`, a `console.context` you can reach from the prompt, and all four storage -adapters registered against real AsyncStorage, MMKV, SecureStore and an in-memory `Map`. +a `console.context` you can reach from the prompt, and all four storage adapters registered against +real AsyncStorage, MMKV, SecureStore and an in-memory `Map`. AsyncStorage and SecureStore ship inside Expo Go, so `bun run start` exercises them as-is. MMKV does diff --git a/content/docs/expo-devtools/in-app-browsers.mdx b/content/docs/expo-devtools/in-app-browsers.mdx index 92e77c5..38d4380 100644 --- a/content/docs/expo-devtools/in-app-browsers.mdx +++ b/content/docs/expo-devtools/in-app-browsers.mdx @@ -1,59 +1,67 @@ --- title: In-app browsers -description: Two props on the WebView, one declared name, and the page's requests and logs join your app's. +description: One hook on the WebView, and the page's requests and logs join your app's. --- A `` runs its own separate JavaScript, in a separate engine, invisible to everything that -patches `fetch` and `console` in your app. So it needs two props wired up: +patches `fetch` and `console` in your app. So it needs wiring of its own, and one hook returns every +prop it takes: ```tsx +import { useDevtoolsWebView } from '@axonpack/expo-devtools'; import { WebView } from 'react-native-webview'; -import { devtools } from './devtools'; - devtools.handleWebViewMessage(event)} -/>; -``` - -Declare the name up front, so a typo cannot silently swallow everything: +export function Checkout() { + const devtoolsWebView = useDevtoolsWebView('checkout'); -```ts title="devtools.ts" -export const devtools = createDevtoolsClient({ - webviewSources: ['my-webview'], -}); + return ; +} ``` That covers both the page's **requests and its console output**. Rows show up tagged -`WebView::[my-webview]` in either tab, and the Source chips can filter them apart from your app's own. +`WebView::[checkout]` in either tab, and the Source chips can filter them apart from your app's own. -`webviewSources` uses a TypeScript `const` type parameter, so the literal names flow into the helpers' -parameter types: passing an undeclared name is a compile error, and at runtime a message from an -undeclared source is dropped. +The name is yours to pick and it is only a label. Name each WebView when the app has more than one; +a single WebView can call the hook with no argument, which labels it `webview`. - - The latter runs after the page's own scripts have already fired, so their requests escape. + + Setting your own replaces the instrumentation, and the page's early requests escape. Your own script + belongs in `injectedJavaScript`, which runs later. -## Optional: reaching the page with throttling +## What the props do -Three more props, only needed if you want the connection settings to apply to the page too: +| Prop | What it buys | +| --------------------------------------- | ---------------------------------------------------------------------------- | +| `injectedJavaScriptBeforeContentLoaded` | The page's `fetch`, `XMLHttpRequest`, `WebSocket`, `EventSource` and `console` | +| `onMessage` | Receives what the page reports. Without it nothing arrives | +| `ref` | A conditions change reaches an already-open page | +| `userAgent` | The browser override in Network conditions applies for real | +| `onShouldStartLoadWithRequest` | Navigation is blocked while Offline is on | -| Prop | Value | What it buys | -| ------------------------------ | ------------------------------------ | --------------------------------------------------------- | -| `ref` | `devtools.getWebViewRef('my-webview')` | A speed change reaches an already-open page | -| `userAgent` | `devtools.getWebViewUserAgent()` | The browser override applies for real | -| `onShouldStartLoadWithRequest` | `devtools.shouldAllowWebViewRequest` | Navigation is blocked while Offline is on | +Everything the hook returns is inert until the devtools are running: the injected script is empty, and +with no `onMessage` behind it `react-native-webview` does not install the page bridge at all. A page can never be *fully* throttled: images, stylesheets and scripts the browser loads by itself still go out at full speed. +## If the page uses postMessage for your own purposes + +Take the handler out and call it first. It returns `true` when the message was one of this package's: + +```tsx + { + if (devtoolsWebView.onMessage(event)) return; + handleMyOwnMessage(event); + }} +/> +``` + ## Next step - + diff --git a/content/docs/expo-devtools/index.mdx b/content/docs/expo-devtools/index.mdx index 89b3f72..cc068ad 100644 --- a/content/docs/expo-devtools/index.mdx +++ b/content/docs/expo-devtools/index.mdx @@ -35,8 +35,8 @@ Two more guides cover things that are not tabs: [themes](/docs/expo-devtools/the a handful of readings dark. - **It depends on no storage library.** The Storage tab reads the stores _you_ register, which is why adding this package cannot drag AsyncStorage or MMKV into your app. -- **`init()` is the only gate.** Ship the code freely: until you call it, nothing is patched, - observed or recorded, and the overlay draws nothing. +- **`config.enabled` is the only gate.** Ship the code freely: with it off, nothing is patched, + observed or recorded, and the provider draws nothing but your app. - **It states its limits.** Every guide here ends with what the tab cannot measure and why, rather than showing a number it had to invent. diff --git a/content/docs/expo-devtools/meta.json b/content/docs/expo-devtools/meta.json index 09da974..cba42c8 100644 --- a/content/docs/expo-devtools/meta.json +++ b/content/docs/expo-devtools/meta.json @@ -24,6 +24,7 @@ "---Reference---", "reference", "---Releases---", + "upgrading", "changelog" ], "defaultOpen": true diff --git a/content/docs/expo-devtools/production.mdx b/content/docs/expo-devtools/production.mdx index 359a259..cbd174a 100644 --- a/content/docs/expo-devtools/production.mdx +++ b/content/docs/expo-devtools/production.mdx @@ -1,40 +1,49 @@ --- title: Leaving it in production -description: Shipping the code is safe. Two switches decide whether anything runs. +description: Shipping the code is safe. One switch decides whether anything runs. --- -Shipping the code is safe. Until `.init()` runs, nothing is patched and nothing is recorded, so the -cost of leaving the package in a production bundle is the bundle size and nothing else. +Shipping the code is safe. There is one switch, `config.enabled`, and with it off nothing is patched +and nothing is recorded, so the cost of leaving the package in a production bundle is the bundle size +and nothing else. -There are two switches, and they do different jobs: +```tsx + + + +``` -- **Capture** — `if (DEVTOOLS_ENABLED) devtools.init();` patches `fetch`, `XMLHttpRequest` and - `console`. Skip it and nothing is ever recorded. -- **Access** — `{DEVTOOLS_ENABLED && }` draws the floating button. Skip it and there - is no way into the panel. +The mount can stay exactly where it is. With `enabled: false` the provider renders its children and +the crash sheet and nothing else: -`DEVTOOLS_ENABLED` is whatever condition you want, evaluated at runtime. +- **No capture.** The `fetch`, `XMLHttpRequest`, `WebSocket` and `console` patches are never + installed, and no store is ever read. +- **No access.** There is no launcher button, and `useDevtoolsPanel().show()` opens nothing. That hook + reports `enabled` so your own trigger can hide itself rather than open an empty panel. + +`enabled` is whatever condition you want, evaluated at runtime. `process.env.EXPO_PUBLIC_APP_ENV !== 'prod'` from the [Quick start](/docs/expo-devtools/quick-start) and `__DEV__` are the two usual choices; anything else works too, including a value you fetch for a specific user. - - It hides itself until `init()` has brought the panel up, so skipping the `init()` call alone is - enough. Guarding both is still worth doing — it keeps the component out of the render tree entirely. + + The patches are global and go in one time, so the config that first render saw is the one that + applies. `enabled` cannot be flipped mid-session, and rebuilding the config object later changes + nothing. That also settles the [Debug tab](/docs/expo-devtools/debug), whose buttons call straight into the native module and are **not** restricted to development builds. They live behind the panel, and the panel is -unreachable without `init()`. +unreachable with the devtools off. ## The two things that do run in production - **Crash reporting**, if you asked for it. `crash: { enableWhileDevtoolsDisabled: true }` installs the - handlers when the client is constructed rather than at `init()`. It is the one subsystem meant to - survive into a release build — see [Crash reporting](/docs/expo-devtools/crash-reporting). -- **The `>` prompt**, if you called `init()`. `console.repl` defaults to `true` and is not gated on - `__DEV__`, so a build that calls `init()` gets a prompt that runs whatever is typed into it. Set - `console: { repl: false }` for any build where that is not what you want. + handlers even with `enabled: false`. It is the one subsystem meant to survive into a release build — + see [Crash reporting](/docs/expo-devtools/crash-reporting). +- **The `>` prompt**, in any build where the devtools are on. `console.repl` defaults to `true` and is + not gated on `__DEV__`, so a build with `enabled: true` gets a prompt that runs whatever is typed + into it. Set `console: { repl: false }` for any build where that is not what you want. ## Next step diff --git a/content/docs/expo-devtools/quick-start.mdx b/content/docs/expo-devtools/quick-start.mdx index 1d05733..e9f6db0 100644 --- a/content/docs/expo-devtools/quick-start.mdx +++ b/content/docs/expo-devtools/quick-start.mdx @@ -1,27 +1,29 @@ --- title: Quick start -description: Create the client, call init() once at startup, and mount the overlay once at the root. +description: Wrap your app in the provider once, at the root. That is the whole setup. --- -Two things have to happen: `init()` runs **once at startup**, and `` is mounted -**once at the root**. Nothing else. +One thing has to happen: `` wraps your app, **once, at the root**. It starts the +devtools and hosts the panel. Nothing else. -## 1. Create the client +## 1. Keep the config in its own file -One shared instance the rest of your app imports, plus one flag deciding whether it runs at all: +One object the root imports, with the flag that decides whether any of this runs: ```ts title="devtools.ts" -import { createDevtoolsClient } from '@axonpack/expo-devtools'; +import type { DevtoolsConfig } from '@axonpack/expo-devtools'; -export const DEVTOOLS_ENABLED = process.env.EXPO_PUBLIC_APP_ENV !== 'prod'; - -export const devtools = createDevtoolsClient(); +export const devtoolsConfig = { + enabled: process.env.EXPO_PUBLIC_APP_ENV !== 'prod', +} satisfies DevtoolsConfig; ``` Set `EXPO_PUBLIC_APP_ENV=prod` for your production builds (in `eas.json`, or a `.env` file) and leave -it unset everywhere else. Use `__DEV__` instead if a dev/release split is all you need. +it unset everywhere else. Use `__DEV__` instead if a dev/release split is all you need. An inline +object on the provider works just as well; a file of its own only keeps a long config out of your +root layout. -## 2. Wire it up +## 2. Wrap your app Use whichever of these matches your app. You only need one. @@ -29,22 +31,18 @@ Use whichever of these matches your app. You only need one. -The root layout is the place. `devtools.init()` goes at **module scope**, outside the component, so -the `fetch` and `console` patches are installed before the first screen renders. +The root layout is the place. One provider there covers every route. ```tsx title="app/_layout.tsx" import { Stack } from 'expo-router'; -import { DevtoolsOverlay } from '@axonpack/expo-devtools'; -import { devtools, DEVTOOLS_ENABLED } from '../devtools'; - -if (DEVTOOLS_ENABLED) devtools.init(); +import { DevtoolsProvider } from '@axonpack/expo-devtools'; +import { devtoolsConfig } from '../devtools'; export default function RootLayout() { return ( - <> + - {DEVTOOLS_ENABLED && } - + ); } ``` @@ -53,28 +51,17 @@ export default function RootLayout() { -`init()` goes in the entry file, before the app is registered. The overlay goes in your root -component. - -```ts title="index.ts" -import { registerRootComponent } from 'expo'; -import App from './App'; -import { devtools, DEVTOOLS_ENABLED } from './devtools'; - -if (DEVTOOLS_ENABLED) devtools.init(); -registerRootComponent(App); -``` +Your root component is the place. ```tsx title="App.tsx" -import { DevtoolsOverlay } from '@axonpack/expo-devtools'; -import { DEVTOOLS_ENABLED } from './devtools'; +import { DevtoolsProvider } from '@axonpack/expo-devtools'; +import { devtoolsConfig } from './devtools'; export default function App() { return ( - <> + - {DEVTOOLS_ENABLED && } - + ); } ``` @@ -90,67 +77,76 @@ the app is running. ## Things that trip people up -- **Mount the overlay exactly once.** The root is the place, because one mount there covers every +- **Mount the provider exactly once.** The root is the place, because one mount there covers every route: the panel opens as a modal on top of whichever screen is showing, so nested Tabs and Drawer - layouts are already covered and must not mount their own. A second mount gives you a second button. -- **`init()` runs exactly once too**, at module scope rather than in a `useEffect`. Anything that - fires before an effect would run — requests during module evaluation, logs at import time — is - missed otherwise. + layouts are already covered and must not mount their own. A second provider gives you a second + button. +- **The patches go in as the provider renders**, not in an effect, which is earlier than any child's + mount and so catches what the first screen requests. Earlier still is out of reach: anything during + module evaluation, before React renders at all, happens before this package can see it. +- **The config is read once.** The first render is what configures everything, because the patches + are global and go in one time. Changing the object later has no effect, so `enabled` cannot be + flipped at runtime. - **Performance starts paused.** Measuring is not free, so press its record button when you want it. The other two recording tabs record from launch. - **Expo Go works.** See [Installation](/docs/expo-devtools/installation) for the handful of readings that go quiet there. -- **In-app browser pages need two extra props** on the `` itself. See +- **In-app browser pages need one hook** on the `` itself. See [In-app browsers](/docs/expo-devtools/in-app-browsers). -- **`init()` is the guard.** `` draws nothing until `init()` has brought the panel - up, so an unguarded mount in a release build is harmless rather than a button over empty lists. - Crash reports still surface, because that is the one subsystem meant to run in production. +- **`enabled: false` is the guard**, and the mount can stay where it is: the provider then renders its + children and nothing else, so there is no button, no panel and nothing patched. Crash reports can + still surface, because that is the one subsystem meant to run in production. See + [Production](/docs/expo-devtools/production). -## Optional: starting before Expo Router +## Opening the panel without the button -Skip this unless you need it. Step 2 is enough for normal use. +The floating button is optional. Turn it off and open the panel from your own UI instead: a long-press +on a header, a row in a staff-only settings screen, a gesture nobody will find by accident. -The root layout runs after Expo Router's own entry file, so requests and logs from that window are -missed, and the startup breakdown's *App setup* phase starts later than the app really did. You can -move `init()` ahead of Expo Router by owning the entry file yourself. - -Point `main` at your own file: - -```json title="package.json" -{ "main": "index.js" } +```tsx + + + ``` -Then have that file call `init()` before handing control to Expo Router. The import order is the whole -point, so keep `init()` in a separate module rather than calling it inline: an `import` is hoisted -above statements in the same file, which would put `expo-router/entry` first anyway. +```tsx title="SettingsRow.tsx" +import { useDevtoolsPanel } from '@axonpack/expo-devtools'; -```js title="index.js" -import './devtools-init'; // a module whose only job is `devtools.init()` -import 'expo-router/entry'; +export function SettingsRow() { + const panel = useDevtoolsPanel(); + if (!panel.enabled) return null; + + return