Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
61d3378
feat: merge the docs site in and make this one project
abappi19 Sep 10, 2026
d852b55
fix: let the dev server serve chunks to LAN origins
abappi19 Sep 11, 2026
8ac03b8
docs: correct the submodule mount path in the config comment
abappi19 Sep 11, 2026
3dc75d9
chore: rename the package to @axonpack/docs
abappi19 Sep 11, 2026
f2897e3
refactor: move release notes onto each library's own changelog page
abappi19 Sep 11, 2026
c015590
fix: repair the deploy workflow left stale by the next migration
abappi19 Sep 11, 2026
dd8dcec
fix: give the header its grid area and move search and theme into the…
abappi19 Sep 11, 2026
44dbe1d
fix: restore the dot to the logo and swap it with the theme
abappi19 Sep 11, 2026
35ff391
feat: give the hero a new slogan and an animated devtools panel
abappi19 Sep 11, 2026
dc7b358
refactor: let the devtools panel own its name and its motion
abappi19 Sep 11, 2026
414cf91
feat: centre the hero and turn the panel into a devtools window
abappi19 Sep 11, 2026
d7a5764
feat: give the hero a sky, a horizon and its own eyebrow
abappi19 Sep 11, 2026
1f1da84
chore: park the hero behind a blank one and flatten the page
abappi19 Sep 11, 2026
c66fc04
feat: give the hero a plain design and drop the scenery
abappi19 Sep 11, 2026
35247c4
feat: generate a changelog page for react-pretty-print
abappi19 Sep 11, 2026
7bbbb5d
refactor: drop the eyebrow pill from the hero
abappi19 Sep 11, 2026
3cf5cb9
refactor: keep the landing header on the landing page
abappi19 Sep 11, 2026
4ad8eb0
docs: write the react-pretty-print pages
abappi19 Sep 11, 2026
ef022e3
feat: give each library its own docs sidebar
abappi19 Sep 11, 2026
29b208f
fix: point the docs links at the /docs segment
abappi19 Sep 11, 2026
596e516
feat: count every download, not the last seven days
abappi19 Sep 11, 2026
086ee57
fix: keep one list of what is published
abappi19 Sep 11, 2026
d9f37a9
refactor: label packages by name rather than by slug
abappi19 Sep 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,27 +41,27 @@ jobs:
uses: actions/configure-pages@v5

# Must run before the typecheck. src/generated/packages.json is gitignored because it is
# rebuilt from npm every time, so in a fresh clone it does not exist yet and
# packages.service.ts cannot resolve its import. Locally this passes only because a build
# has already happened.
- name: Fetch the catalogue from npm
run: bun run packages
# rebuilt from npm every time, so in a fresh clone it does not exist and packages.service.ts
# cannot resolve its import. Locally this passes only because a build already ran.
#
# This also refreshes the committed changelog page, which is what lets the nightly cron pick
# up a release published since the last commit.
- name: Generate the catalogue and changelogs from npm
run: bun run generate

# astro check needs @astrojs/check, which is a devDependency for exactly this reason.
# Without it the command tries to install interactively and hangs the job.
- name: Typecheck
run: bun run typecheck
run: bun run check-types

# Runs scripts/fetch-packages.mjs first, so the catalogue, versions, download counts and
# changelogs are read from npm on every deploy. No credentials: every source is public.
# `generate` runs again here as part of the build script. It is cheap and keeps a local
# `bun run build` self-contained. No credentials: every source is public.
- name: Build
run: bun run build

- name: Upload artifact
if: github.event_name != 'pull_request'
uses: actions/upload-pages-artifact@v3
with:
path: dist
path: out

deploy:
name: Deploy
Expand Down
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,10 @@ dist
*.tsbuildinfo
# Regenerated from npm on every build — never hand-edited, so never committed.
src/generated/packages.json

# Next and fumadocs build output
.next/
out/
.source
next-env.d.ts
.migration-keep
15 changes: 15 additions & 0 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["typescript", "react", "import", "nextjs", "unicorn"],
"env": { "builtin": true, "es2022": true },
"settings": { "react": { "version": "19.2" } },
"rules": {
"eqeqeq": ["warn", "smart"],
"no-debugger": "warn",
"no-var": "warn",
"prefer-const": "warn",
"import/no-duplicates": "error",
"typescript/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }]
},
"ignorePatterns": [".next", ".source", "out", "next-env.d.ts", "node_modules"]
}
6 changes: 6 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"printWidth": 100,
"tabWidth": 2,
"singleQuote": true,
"trailingComma": "all"
}
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Axonpack

**The open source tools and libraries for React and React Native.**
**Axonpack ends the guesswork when things break.**

Small, focused, dependency-light packages. Drop one in and carry on. There is no framework here
to adopt.
Answers you and your agent can both read. Every request, error, log and stored value, captured on
the device and copied out as structured JSON.

Live at [axonpack.github.io](https://axonpack.github.io). Docs at
[axonpack.github.io/docs](https://axonpack.github.io/docs).
Expand Down
16 changes: 0 additions & 16 deletions astro.config.mjs

This file was deleted.

895 changes: 373 additions & 522 deletions bun.lock

Large diffs are not rendered by default.

15 changes: 0 additions & 15 deletions components.json

This file was deleted.

272 changes: 272 additions & 0 deletions content/docs/expo-devtools/changelog.mdx

Large diffs are not rendered by default.

72 changes: 72 additions & 0 deletions content/docs/expo-devtools/compatibility.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
title: Compatibility
description: The versions this is built against, what it requires you to have installed, and which platforms it runs on.
---

## Versions

<LatestRelease slug="expo-devtools" />

| `@axonpack/expo-devtools` | Expo SDK | React Native | React |
| ------------------------- | -------- | ------------ | ----- |
| 2.5.x | 57 | 0.86 | 19.2 |

That row is what the package is **built and tested against** — the versions in its own dev
dependencies and in the example app that exercises every tab.

<Callout type="info" title="Nothing here pins you to one SDK">
Every Expo package this needs is a peer dependency, so each resolves to the version your own SDK
ships. The row above is what the package is built and tested against, not a range it refuses to run
outside.
</Callout>

## What you have to install

| Package | Why |
| -------------------------------- | ------------------------------------------------------------ |
| `react-native-safe-area-context` | The overlay and panel lay themselves out inside the safe area |
| `react-native-webview` | HTML and image response previews in the Network tab |
| `expo-clipboard` | Every Copy button, in every tab |

Both are peer dependencies, so they come from your app rather than from this package:

```sh
npx expo install @axonpack/expo-devtools react-native-safe-area-context react-native-webview expo-clipboard
```

Every peer range is `*`. That is deliberate: the point of a peer is that your SDK decides the
version, and a narrower range here would fight it. It does mean npm will not warn you when you are
outside what has been tested, so treat the table above, not the peer range, as the answer.

`expo-clipboard` is a peer rather than a dependency for that reason — as a dependency it would have
to name a version, and any version it named would tie the package to one SDK line. React Native does
still ship a built-in `Clipboard`, but it is deprecated, slated for removal, and warns through
`console.warn`, which this package patches — so its own deprecation notice would appear in its own
Console tab.

## Platforms

| Platform | State |
| ----------- | ------------------------------------------------------------------------- |
| **iOS** | Full support, native module included |
| **Android** | Full support, native module included |

The native module is written once per platform and is loaded **optionally**, so the panel itself is
plain JavaScript and never depends on it existing. That is what makes Expo Go work — see
[What needs a development build](/docs/expo-devtools/reference/development-build) for the handful of
readings that go quiet there.

## Storage libraries

The Storage tab reads whatever you register, so its compatibility is really the adapters'. Versions
the example app is built against:

| Library | Versions handled |
| --------------------------------------------- | --------------------------------------------------------------------- |
| `@react-native-async-storage/async-storage` | v1, v2 and v3 — `getMany` and `multiGet` are both accepted |
| `react-native-mmkv` | v3 and v4 — `delete` and `remove` are both accepted |
| `expo-secure-store` | SDK 57 |
| anything else | via `defineStorageAdapter`, which duck-types nothing |

MMKV and SecureStore carry native code of their own, so a store that needs a development build to
exist also needs one to be inspectable. An adapter over an in-memory `Map` works in Expo Go.
74 changes: 74 additions & 0 deletions content/docs/expo-devtools/console.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
title: Console
description: Everything your app logs, on the device, plus a prompt that runs JavaScript and answers.
---

The Console tab mirrors everything your app logs, on the device. Warnings sit on a yellow row and
errors on a red one, and the toolbar keeps a running count of each — so you can see at a glance
whether anything went wrong while you were not looking.

![Console tab listing captured logs](/expo-devtools/screenshots/console-log.png)

- Each thing you logged gets its own line, so a message and the object next to it do not run together.
Objects and arrays start collapsed. Tap to open them up, level by level.
- Errors show their message on the row and the **full stack** when you tap it.
- The same message logged over and over becomes **one row with a count**, so a chatty screen does not
bury everything else.
- Filter by level (each chip carries a live count) or by source, or search the text of every message —
with match case, whole word and regex, and every match highlighted where it sits.
- The newest output stays in view automatically, and stops following if you scroll back to read
something, with a button to jump back to the newest.
- Copy any line with one tap.

![Level filter chips showing live counts](/expo-devtools/screenshots/console-filters.png)

## Running an expression

![An expanded error stack and name suggestions at the prompt](/expo-devtools/screenshots/console-autocomplete.png)

The `>` prompt at the bottom runs JavaScript on the device and shows you what came back. Your typed
command appears with a `›`, the answer with a `‹`, and objects come back as the same explorable tree.
Something that returns a promise shows as pending and fills in when it settles, so
`fetch(...).then((r) => r.json())` works as you would expect.

- Names are **suggested as you type**, including the members of whatever object you are inside.
- **Tap any command you ran earlier** to load it straight back into the prompt.
- Two built-in helpers reach your app's own code in a development build: `$modules('auth')` lists the
files that are loaded, and `$m('src/stores/auth')` hands you one of them.

![The prompt answering an expression](/expo-devtools/screenshots/console-repl.png)

### Reaching your own objects

Your app's files are bundled as private closures, so nothing can reach an imported name on its own the
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 } },
});
```

It is also the only thing that works in a release build, where the module list the two helpers above
read is not available.

<Callout type="warn" title="The prompt is on by default, in every build">
`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 }`.
</Callout>

## Limits

- The list holds the **500** most recent rows.
- Only `console.log`, `.info`, `.warn`, `.error` and `.debug` are mirrored.
- The prompt cannot reach bundled module names on its own. `console.context` is the supported route.

## Next step

<Cards>
<Card title="Console tab reference" href="/expo-devtools/reference/console-tab" description="Every control, chip and row element." />
<Card title="Crash reporting" href="/expo-devtools/crash-reporting" description="What happens when a log becomes a crash." />
</Cards>
132 changes: 132 additions & 0 deletions content/docs/expo-devtools/crash-reporting.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
---
title: Crash reporting
description: JS errors, unhandled rejections, render errors and native exceptions, turned into a report you can read on the device.
---

The Crashes tab turns the errors that end a session, or nearly do, into a report you can read on the
device. It is also the one subsystem here meant to survive into a release build.

A report carries the message, the stack, the component stack where there is one, breadcrumbs, device
details and the raw JSON. You can copy it as Markdown or JSON, or share the whole thing. Past reports
stay in a history with an unread count on the tab.

## The four tiers

Which tier caught a crash decides how much it can say.

| Tier | Where it comes from |
| ------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| **JS errors** | The global `ErrorUtils` handler, *wrapped* rather than replaced, so LogBox and React Native's own reporting live |
| **Unhandled promise rejections** | The Hermes rejection tracker. React Native registers its own only in development |
| **React render errors** | The exported `<DevtoolsErrorBoundary />`. The only tier that produces a component stack |
| **Uncaught native exceptions** | The platform's uncaught-exception handler on each side, chained to whatever was installed before it |

## 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:

```ts title="devtools.ts"
export const devtools = createDevtoolsClient({
crash: { enableWhileDevtoolsDisabled: true },
});
```

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
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.

<Callout type="info" title="Two sheets, and the wrong one in release is a real problem">
`popupDetail` defaults to `'auto'`, which picks between two sheets. With the devtools enabled you get
the full developer sheet: tabs, a stack tree, raw JSON and this package's branding. With them
disabled you get a compact notice: what broke, when, and Share / Copy / Dismiss. Set it explicitly
for an internal build that ships the crash sheet but not the panel and still wants the stack on
screen.
</Callout>

If you ship crash reporting without the panel, mount the sheet yourself:

```tsx
import { CrashReportOverlay } from '@axonpack/expo-devtools';
```

`<DevtoolsOverlay />` already mounts one, and mounting both is harmless: whichever mounted first owns
the sheet and the other draws nothing.

## Catching render errors

`<DevtoolsErrorBoundary />` is the only tier that produces a component stack, and it turns a white
screen into a Try again button — which is why it is worth mounting even in a release build:

```tsx
import { DevtoolsErrorBoundary } from '@axonpack/expo-devtools';

<DevtoolsErrorBoundary
fallback={(error, reset) => <MyErrorScreen error={error} onRetry={reset} />}
onError={(error, info) => report(error, info)}
>
<Checkout />
</DevtoolsErrorBoundary>;
```

`fallback` replaces the built-in screen and `reset` remounts the subtree that threw. A boundary around
a subtree is the stronger tool wherever it fits, because unmounting that subtree actually discards the
broken state rather than stepping over it.

## Attaching your own details

```ts
devtools.setCrashContext({ userId, screen, flags });
```

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),
},
});
```

## Decisions worth knowing

- **A fatal JS error does not end the app, and there is no switch for that.** React Native decides this
by build: in development it hands the error to the red box and tells the native side nothing; in a
release build it reports it, which is what ends the process. The same error, the same fatality — only
the branch differs. Capturing it closes that gap. Turning the `jsErrors` tier off hands the decision
back to React Native.
- **What survives is the process, not necessarily the state.** The JavaScript thread was interrupted
part-way through, possibly mid-render, so component state, the native view tree and your own state
may afterwards disagree. This is why the report is put on screen rather than filed quietly.
- **A crash that killed the app is reported at the next launch.** A dying process is written from
native, on the dying thread, into the app's own sandbox, and drained at the next launch — which is
also the proof the process died. Non-fatal records are not persisted: the app survived them, so
re-reporting one next launch would be a bug. `persistNonFatal` turns that on if you want it.
- **Stacks are symbolicated by asking Metro**, the way LogBox does, and only when the trace itself came
from an http origin. A release build asks nobody.
- **Breadcrumbs carry request URLs and whatever the app logged**, which is a different privacy
proposition from a stack trace. They default to on; `crash: { breadcrumbs: false }` turns them off.

## Limits

- **No backend.** Nothing is sent anywhere. `onCrash` is the hook if you want to send reports
yourself — queueing and retry are yours to write.
- **No grouping.** Duplicate crashes are one row each.
- **The current route is not captured automatically.** Pass it through `setCrashContext`.

## Next step

<Cards>
<Card title="Client reference" href="/expo-devtools/reference/client#crash" description="Every crash option and its default." />
<Card title="Leaving it in production" href="/expo-devtools/production" description="What ships, and what stays dark." />
</Cards>
Loading
Loading