Skip to content
Open
12 changes: 12 additions & 0 deletions .changeset/event-client-production-stripping.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@tanstack/devtools-event-client': minor
---

The root export of `@tanstack/devtools-event-client` now resolves to a no-op
outside development (`process.env.NODE_ENV !== 'development'`), so the real
`EventClient` is tree-shaken out of production bundles by default.

If you want devtools events to keep working in production, import the real
client from the new `@tanstack/devtools-event-client/production` subpath, which
always ships the real implementation. The public API is identical between the
two imports.
24 changes: 24 additions & 0 deletions docs/building-custom-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,3 +209,27 @@ For usage details, see the [Using devtools-utils](./devtools-utils) guide.
Once your plugin is working, you can share it with the community by publishing it to npm and submitting it to the TanStack Devtools Marketplace. The marketplace is a registry of third-party plugins that users can discover and install directly from the devtools UI.

For submission instructions and the registry format, see [Third-party Plugins](./third-party-plugins).

## Production builds

By default the **root import** of `@tanstack/devtools-event-client` no-ops
outside development. When your bundler sets `process.env.NODE_ENV` to anything
other than `'development'`, the real client is replaced by a no-op and
tree-shaken out of your production bundle:

```ts
// dev: real client — production: no-op (tree-shaken away)
import { EventClient } from '@tanstack/devtools-event-client'
```

If you are an open-source author who deliberately wants devtools events in
production, import the real client from the `/production` subpath instead. It is
never stripped:

```ts
// always the real client, in every environment
import { EventClient } from '@tanstack/devtools-event-client/production'
```

The public API is identical between the two imports — only the production
runtime behavior differs.
24 changes: 24 additions & 0 deletions docs/framework/angular/guides/custom-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,3 +215,27 @@ Heres an example of both:

[tanstack-devtools:custom-devtools-plugin] Registered event to bus custom-devtools:counter-state
```

## Production builds

By default the **root import** of `@tanstack/devtools-event-client` no-ops
outside development. When your bundler sets `process.env.NODE_ENV` to anything
other than `'development'`, the real client is replaced by a no-op and
tree-shaken out of your production bundle:

```ts
// dev: real client — production: no-op (tree-shaken away)
import { EventClient } from '@tanstack/devtools-event-client'
```

If you are an open-source author who deliberately wants devtools events in
production, import the real client from the `/production` subpath instead. It is
never stripped:

```ts
// always the real client, in every environment
import { EventClient } from '@tanstack/devtools-event-client/production'
```

The public API is identical between the two imports — only the production
runtime behavior differs.
24 changes: 24 additions & 0 deletions docs/framework/preact/guides/custom-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,3 +202,27 @@ Heres an example of both:

🌴 [tanstack-devtools:custom-devtools-plugin] Registered event to bus custom-devtools:counter-state
```

## Production builds

By default the **root import** of `@tanstack/devtools-event-client` no-ops
outside development. When your bundler sets `process.env.NODE_ENV` to anything
other than `'development'`, the real client is replaced by a no-op and
tree-shaken out of your production bundle:

```ts
// dev: real client — production: no-op (tree-shaken away)
import { EventClient } from '@tanstack/devtools-event-client'
```

If you are an open-source author who deliberately wants devtools events in
production, import the real client from the `/production` subpath instead. It is
never stripped:

```ts
// always the real client, in every environment
import { EventClient } from '@tanstack/devtools-event-client/production'
```

The public API is identical between the two imports — only the production
runtime behavior differs.
24 changes: 24 additions & 0 deletions docs/framework/react/guides/custom-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,3 +199,27 @@ Heres an example of both:
🌴 [tanstack-devtools:custom-devtools-plugin] Registered event to bus custom-devtools:counter-state
```

## Production builds

By default the **root import** of `@tanstack/devtools-event-client` no-ops
outside development. When your bundler sets `process.env.NODE_ENV` to anything
other than `'development'`, the real client is replaced by a no-op and
tree-shaken out of your production bundle:

```ts
// dev: real client — production: no-op (tree-shaken away)
import { EventClient } from '@tanstack/devtools-event-client'
```

If you are an open-source author who deliberately wants devtools events in
production, import the real client from the `/production` subpath instead. It is
never stripped:

```ts
// always the real client, in every environment
import { EventClient } from '@tanstack/devtools-event-client/production'
```

The public API is identical between the two imports — only the production
runtime behavior differs.
24 changes: 24 additions & 0 deletions docs/framework/solid/guides/custom-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,3 +195,27 @@ Heres an example of both:

[tanstack-devtools:custom-devtools-plugin] Registered event to bus custom-devtools:counter-state
```

## Production builds

By default the **root import** of `@tanstack/devtools-event-client` no-ops
outside development. When your bundler sets `process.env.NODE_ENV` to anything
other than `'development'`, the real client is replaced by a no-op and
tree-shaken out of your production bundle:

```ts
// dev: real client — production: no-op (tree-shaken away)
import { EventClient } from '@tanstack/devtools-event-client'
```

If you are an open-source author who deliberately wants devtools events in
production, import the real client from the `/production` subpath instead. It is
never stripped:

```ts
// always the real client, in every environment
import { EventClient } from '@tanstack/devtools-event-client/production'
```

The public API is identical between the two imports — only the production
runtime behavior differs.
24 changes: 24 additions & 0 deletions docs/framework/vue/guides/custom-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,3 +202,27 @@ Heres an example of both:

[tanstack-devtools:custom-devtools-plugin] Registered event to bus custom-devtools:counter-state
```

## Production builds

By default the **root import** of `@tanstack/devtools-event-client` no-ops
outside development. When your bundler sets `process.env.NODE_ENV` to anything
other than `'development'`, the real client is replaced by a no-op and
tree-shaken out of your production bundle:

```ts
// dev: real client — production: no-op (tree-shaken away)
import { EventClient } from '@tanstack/devtools-event-client'
```

If you are an open-source author who deliberately wants devtools events in
production, import the real client from the `/production` subpath instead. It is
never stripped:

```ts
// always the real client, in every environment
import { EventClient } from '@tanstack/devtools-event-client/production'
```

The public API is identical between the two imports — only the production
runtime behavior differs.
26 changes: 26 additions & 0 deletions docs/production.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,32 @@ function App() {
}
```

## Event client in production

By default the **root import** of `@tanstack/devtools-event-client` no-ops
outside development. When your bundler sets `process.env.NODE_ENV` to anything
other than `'development'`, the real client is replaced by a no-op and
tree-shaken out of your production bundle:

```ts
// dev: real client — production: no-op (tree-shaken away)
import { EventClient } from '@tanstack/devtools-event-client'
```

If you are an open-source author who deliberately wants devtools events in
production, import the real client from the `/production` subpath instead. It is
never stripped:

```ts
// always the real client, in every environment
import { EventClient } from '@tanstack/devtools-event-client/production'
```

The public API is identical between the two imports — only the production
runtime behavior differs.

This is independent of the Vite plugin's `removeDevtoolsOnBuild` option — the event client strips itself based on `NODE_ENV`, whether or not you use the Vite plugin.

## Where to install the Devtools

If you are using the devtools in development only, you can install them as a development dependency and only import them in development builds. This is the default recommended way to use the devtools.
Expand Down
26 changes: 26 additions & 0 deletions packages/event-bus-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,29 @@ plugin.on('b', (e) => {
The `EventClient` class is a base class for creating plugins that can subscribe to events in the TanStack Devtools event bus. It allows you to define a set of events and their corresponding data schemas using a standard-schema based schemas.

It will work on both the client/server side and all you have to worry about is emitting/listening to events.

## Production builds

By default the **root import** of `@tanstack/devtools-event-client` no-ops
outside development. When your bundler sets `process.env.NODE_ENV` to anything
other than `'development'`, the real client is replaced by a no-op and
tree-shaken out of your production bundle:

```ts
// dev: real client — production: no-op (tree-shaken away)
import { EventClient } from '@tanstack/devtools-event-client'
```

If you are an open-source author who deliberately wants devtools events in
production, import the real client from the `/production` subpath instead. It is
never stripped:

```ts
// always the real client, in every environment
import { EventClient } from '@tanstack/devtools-event-client/production'
```

The public API is identical between the two imports — only the production
runtime behavior differs.

"Outside development" includes when `NODE_ENV` is unset — common in plain Node scripts, some SSR dev servers, and test runners — so the root import resolves to the no-op there too. Set `NODE_ENV=development`, or use the `/production` subpath, to get the real client in those contexts.
10 changes: 10 additions & 0 deletions packages/event-bus-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@
"default": "./dist/cjs/index.cjs"
}
},
"./production": {
"import": {
"types": "./dist/esm/production.d.ts",
"default": "./dist/esm/production.js"
},
"require": {
"types": "./dist/cjs/production.d.cts",
"default": "./dist/cjs/production.cjs"
}
},
"./package.json": "./package.json"
},
"bin": {
Expand Down
31 changes: 16 additions & 15 deletions packages/event-bus-client/skills/devtools-event-client/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Install the package:
npm i @tanstack/devtools-event-client
```

The package exports a single class:
The package has two entry points — the root export (the real client in development, a no-op tree-shaken out of production), and the `/production` subpath (always the real client, for libraries that want devtools events in production):

```ts
import { EventClient } from '@tanstack/devtools-event-client'
Expand Down Expand Up @@ -257,30 +257,31 @@ storeInspector.emit('state-changed', {
})
```

### 7. Not stripping EventClient emit calls for production (HIGH)
### 7. Forgetting the root export no-ops in production (HIGH)

The Vite plugin strips adapter imports (e.g., `@tanstack/react-devtools`) from production builds, but it does NOT strip `@tanstack/devtools-event-client` imports or `emit()` calls. Library authors must guard emit calls themselves.
The **root import** of `@tanstack/devtools-event-client` resolves to a no-op
when `process.env.NODE_ENV !== 'development'`, and the real client is
tree-shaken out of production bundles. This is the default and what you want for
most libraries — your `emit()` calls cost nothing in production.

Options:

**Option A:** Use the `enabled` constructor option:
"Outside development" includes when `NODE_ENV` is unset — common in plain Node scripts, some SSR dev servers, and test runners — so the root import resolves to the no-op there too. Set `NODE_ENV=development`, or use the `/production` subpath, to get the real client in those contexts.

```ts
super({
pluginId: 'store-inspector',
enabled: process.env.NODE_ENV !== 'production',
})
// dev: real client — production: no-op, removed from the bundle
import { EventClient } from '@tanstack/devtools-event-client'
```

**Option B:** Conditional guard at the call site:
If you are publishing an open-source library and deliberately want devtools
events to keep working in production, import from the `/production` subpath,
which always ships the real client:

```ts
if (process.env.NODE_ENV !== 'production') {
storeInspector.emit('state-changed', data)
}
import { EventClient } from '@tanstack/devtools-event-client/production'
```

When `enabled` is `false`, `emit()` returns immediately (no event creation, no queuing, no connection attempt). This is the preferred approach.
The `enabled` constructor option still works for fine-grained runtime control,
but you no longer need to guard `emit()` calls manually for bundle size — the
root export handles that for you.

## See Also

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ if (process.env.NODE_ENV !== 'production') {
}
```

**Important:** The Vite plugin strips `@tanstack/react-devtools` from production but does NOT strip `@tanstack/devtools-event-client`. You must guard yourself.
**Important:** The Vite plugin strips `@tanstack/react-devtools` from production. The root import of `@tanstack/devtools-event-client` also no-ops and is tree-shaken out when `process.env.NODE_ENV !== 'development'`, so `emit()` calls cost nothing in production by default. Import from `@tanstack/devtools-event-client/production` if you deliberately want events in production. The `enabled` option remains available for runtime control.

### 6. Server/Client Transparent Bridging

Expand Down
20 changes: 19 additions & 1 deletion packages/event-bus-client/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,19 @@
export { EventClient } from './plugin'
import { EventClient as EventClientImpl } from './plugin'
import { EventClientNoOp } from './noop'

/**
* The real `EventClient` in development; a no-op everywhere else.
*
* Production bundlers replace `process.env.NODE_ENV` with a literal, fold this
* ternary to `EventClientNoOp`, and tree-shake `./plugin` out of the bundle.
* To keep the real client in production, import it from
* `@tanstack/devtools-event-client/production` instead.
*/
const EventClient = (process.env.NODE_ENV !== 'development'
? EventClientNoOp
: EventClientImpl) as unknown as typeof EventClientImpl

type EventClient<TEventMap extends Record<string, any>> =
EventClientImpl<TEventMap>

export { EventClient }
Loading
Loading