bippy 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright 2024 Aiden Bai
1
+ Copyright 2024 Aiden Bai, Million Software, Inc.
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
4
 
package/README.md CHANGED
@@ -9,26 +9,46 @@
9
9
  [![version](https://img.shields.io/npm/v/bippy?style=flat&colorA=000000&colorB=000000)](https://npmjs.com/package/bippy)
10
10
  [![downloads](https://img.shields.io/npm/dt/bippy.svg?style=flat&colorA=000000&colorB=000000)](https://npmjs.com/package/bippy)
11
11
 
12
- a hacky way to get fibers from react. <small>used internally by [`react-scan`](https://github.com/aidenybai/react-scan)</small>
12
+ bippy is a toolkit to **hack into react internals**
13
13
 
14
- bippy _attempts\*_ to solve two problems:
14
+ by default, you cannot access react internals. bippy bypasses this by "pretending" to be react devtools, giving you access to the fiber tree and other internals.
15
15
 
16
- 1. it's not possible to write instrumentation for React without the end user changing code
17
- 2. doing anything useful with fibers requires you to know react source code very well
16
+ - works outside of react no react code modification needed
17
+ - utility functions that work across modern react (v17-19)
18
+ - no prior react source code knowledge required
18
19
 
19
- bippy allows you to access fiber information from outside of react and provides friendly low-level utils for interacting with fibers.
20
+ ```jsx
21
+ import { instrument, traverseFiber } from 'bippy';
20
22
 
21
- <sub><sup>\*disclaimer: "attempt" used loosely, i highly recommend not relying on this in production</sub></sup>
23
+ instrument({
24
+ onCommitFiberRoot(_, root) {
25
+ traverseFiber(root.current, (fiber) => {
26
+ // will print every fiber in the current React tree
27
+ console.log('fiber:', fiber);
28
+ });
29
+ },
30
+ });
31
+ ```
32
+
33
+ <table>
34
+ <tbody>
35
+ <tr>
36
+ <td>
37
+ <a href="https://bippy.dev"><b>open live demo ↗</b></a>
38
+ </td>
39
+ </tr>
40
+ </tbody>
41
+ </table>
22
42
 
23
- ## how it works
43
+ ## how it works & motivation
24
44
 
25
- bippy allows you to **access** and **use** fibers from outside of react.
45
+ bippy allows you to **access** and **use** react fibers **outside** of react components.
26
46
 
27
47
  a react fiber is a "unit of execution." this means react will do something based on the data in a fiber. each fiber either represents a composite (function/class component) or a host (dom element).
28
48
 
29
49
  > here is a [live visualization](https://jser.pro/ddir/rie?reactVersion=18.3.1&snippetKey=hq8jm2ylzb9u8eh468) of what the fiber tree looks like, and here is a [deep dive article](https://jser.dev/2023-07-18-how-react-rerenders/).
30
50
 
31
- fibers are useful because they contain information about the React app (component props, state, contexts, etc.). a simplified version of a fiber looks roughly like this:
51
+ fibers are useful because they contain information about the react app (component props, state, contexts, etc.). a simplified version of a fiber looks roughly like this:
32
52
 
33
53
  ```typescript
34
54
  interface Fiber {
@@ -44,6 +64,9 @@ interface Fiber {
44
64
  // parent fiber
45
65
  return: Fiber | null;
46
66
 
67
+ // the previous or current version of the fiber
68
+ alternate: Fiber | null;
69
+
47
70
  // saved props input
48
71
  memoizedProps: any;
49
72
 
@@ -71,16 +94,16 @@ while all of the information is there, it's not super easy to work with, and cha
71
94
 
72
95
  however, fibers aren't directly accessible by the user. so, we have to hack our way around to accessing it.
73
96
 
74
- luckily, react [reads from a property](https://github.com/facebook/react/blob/6a4b46cd70d2672bc4be59dcb5b8dede22ed0cef/packages/react-reconciler/src/ReactFiberDevToolsHook.js#L48) in the window object: `window.__REACT_DEVTOOLS_GLOBAL_HOOK__` and runs handlers on it when certain events happen. this property must exist before react's bundle is executed. this is intended for react devtools, but we can use it to our advantage.
97
+ luckily, react [reads from a property](https://github.com/facebook/react/blob/6a4b46cd70d2672bc4be59dcb5b8dede22ed0cef/packages/react-reconciler/src/reactFiberDevToolsHook.js#L48) in the window object: `window.__REACT_DEVTOOLS_GLOBAL_HOOK__` and runs handlers on it when certain events happen. this property must exist before react's bundle is executed. this is intended for react devtools, but we can use it to our advantage.
75
98
 
76
99
  here's what it roughly looks like:
77
100
 
78
101
  ```typescript
79
102
  interface __REACT_DEVTOOLS_GLOBAL_HOOK__ {
80
103
  // list of renderers (react-dom, react-native, etc.)
81
- renderers: Map<RendererID, ReactRenderer>;
104
+ renderers: Map<RendererID, reactRenderer>;
82
105
 
83
- // called when react has rendered everything for an update and is ready to
106
+ // called when react has rendered everything for an update and the fiber tree is fully built and ready to
84
107
  // apply changes to the host tree (e.g. DOM mutations)
85
108
  onCommitFiberRoot: (
86
109
  rendererID: RendererID,
@@ -92,7 +115,7 @@ interface __REACT_DEVTOOLS_GLOBAL_HOOK__ {
92
115
  onPostCommitFiberRoot: (rendererID: RendererID, root: FiberRoot) => void;
93
116
 
94
117
  // called when a specific fiber unmounts
95
- onCommitFiberUnmount: (rendererID: RendererID, Fiber: Fiber) => void;
118
+ onCommitFiberUnmount: (rendererID: RendererID, fiber: Fiber) => void;
96
119
  }
97
120
  ```
98
121
 
@@ -102,6 +125,282 @@ bippy works by monkey-patching `window.__REACT_DEVTOOLS_GLOBAL_HOOK__` with our
102
125
  - _(instead of directly mutating `onCommitFiberRoot`, ...)_
103
126
  - `secure` to wrap your handlers in a try/catch and determine if handlers are safe to run
104
127
  - _(instead of rawdogging `window.__REACT_DEVTOOLS_GLOBAL_HOOK__` handlers, which may crash your app)_
128
+ - `createFiberVisitor` to traverse the fiber tree and determine which fibers have actually rendered
129
+ - _(instead of `child`, `sibling`, and `return` pointers)_
130
+ - `traverseFiber` to traverse the fiber tree, regardless of whether it has rendered
131
+ - _(instead of `child`, `sibling`, and `return` pointers)_
132
+ - `setFiberId` / `getFiberId` to set and get a fiber's id
133
+ - _(instead of anonymous fibers with no identity)_
134
+
135
+ ## how to use
136
+
137
+ you can either install via a npm (recommended) or a script tag.
138
+
139
+ this package should be imported before a React app runs. this will add a special object to the global which is used by React for providing its internals to the tool for analysis (React Devtools does the same). as soon as React library is loaded and attached to the tool, bippy starts collecting data about what is going on in React's internals.
140
+
141
+ ```shell
142
+ npm install bippy
143
+ ```
144
+
145
+ or, use via script tag. must be added before any other scripts run:
146
+
147
+ ```html
148
+ <script src="https://unpkg.com/bippy"></script>
149
+ ```
150
+
151
+ > this will cause bippy to be accessible under a `window.Bippy` global.
152
+
153
+ next, you can use the api to get data about the fiber tree. below is a (useful) subset of the api. for the full api, read the [source code](https://github.com/aidenybai/bippy/blob/main/src/core.ts).
154
+
155
+ ### instrument
156
+
157
+ patches `window.__REACT_DEVTOOLS_GLOBAL_HOOK__` with your handlers. must be imported before react, and must be initialized to properly run any other methods.
158
+
159
+ > use with the `secure` function to prevent uncaught errors from crashing your app.
160
+
161
+ ```typescript
162
+ import { instrument, secure } from 'bippy'; // must be imported BEFORE react
163
+ import * as React from 'react';
164
+
165
+ instrument(
166
+ secure({
167
+ onCommitFiberRoot(rendererID, root) {
168
+ console.log('root ready to commit', root);
169
+ },
170
+ onPostCommitFiberRoot(rendererID, root) {
171
+ console.log('root with effects committed', root);
172
+ },
173
+ onCommitFiberUnmount(rendererID, fiber) {
174
+ console.log('fiber unmounted', fiber);
175
+ },
176
+ })
177
+ );
178
+ ```
179
+
180
+ ### createFiberVisitor
181
+
182
+ not every fiber in the fiber tree renders. `createFiberVisitor` allows you to traverse the fiber tree and determine which fibers have actually rendered.
183
+
184
+ ```typescript
185
+ import { instrument, secure, createFiberVisitor } from 'bippy'; // must be imported BEFORE react
186
+ import * as React from 'react';
187
+
188
+ const visit = createFiberVisitor({
189
+ onRender(fiber) {
190
+ console.log('fiber rendered', fiber);
191
+ },
192
+ });
193
+
194
+ instrument(
195
+ secure({
196
+ onCommitFiberRoot(rendererID, root) {
197
+ visit(rendererID, root);
198
+ },
199
+ })
200
+ );
201
+ ```
202
+
203
+ ### traverseFiber
204
+
205
+ calls a callback on every fiber in the fiber tree.
206
+
207
+ ```typescript
208
+ import { instrument, secure, traverseFiber } from 'bippy'; // must be imported BEFORE react
209
+ import * as React from 'react';
210
+
211
+ instrument(
212
+ secure({
213
+ onCommitFiberRoot(rendererID, root) {
214
+ traverseFiber(root.current, (fiber) => {
215
+ console.log(fiber);
216
+ });
217
+ },
218
+ })
219
+ );
220
+ ```
221
+
222
+ ### traverseProps
223
+
224
+ traverses the props of a fiber.
225
+
226
+ ```typescript
227
+ import { traverseProps } from 'bippy';
228
+
229
+ // ...
230
+
231
+ traverseProps(fiber, (propName, next, prev) => {
232
+ console.log(propName, next, prev);
233
+ });
234
+ ```
235
+
236
+ ### traverseState
237
+
238
+ traverses the state (useState, useReducer, etc.) and effects that set state of a fiber.
239
+
240
+ ```typescript
241
+ import { traverseState } from 'bippy';
242
+
243
+ // ...
244
+
245
+ traverseState(fiber, (next, prev) => {
246
+ console.log(next, prev);
247
+ });
248
+ ```
249
+
250
+ ### traverseEffects
251
+
252
+ traverses the effects (useEffect, useLayoutEffect, etc.) of a fiber.
253
+
254
+ ```typescript
255
+ import { traverseEffects } from 'bippy';
256
+
257
+ // ...
258
+
259
+ traverseEffects(fiber, (effect) => {
260
+ console.log(effect);
261
+ });
262
+ ```
263
+
264
+ ### traverseContexts
265
+
266
+ traverses the contexts (useContext) of a fiber.
267
+
268
+ ```typescript
269
+ import { traverseContexts } from 'bippy';
270
+
271
+ // ...
272
+
273
+ traverseContexts(fiber, (next, prev) => {
274
+ console.log(next, prev);
275
+ });
276
+ ```
277
+
278
+ ### setFiberId / getFiberId
279
+
280
+ set and get a persistent identity for a fiber. by default, fibers are anonymous and have no identity.
281
+
282
+ ```typescript
283
+ import { setFiberId, getFiberId } from 'bippy';
284
+
285
+ // ...
286
+
287
+ setFiberId(fiber);
288
+ console.log('unique id for fiber:', getFiberId(fiber));
289
+ ```
290
+
291
+ ### isHostFiber
292
+
293
+ returns `true` if the fiber is a host fiber (e.g., a DOM node in react-dom).
294
+
295
+ ```typescript
296
+ import { isHostFiber } from 'bippy';
297
+
298
+ if (isHostFiber(fiber)) {
299
+ console.log('fiber is a host fiber');
300
+ }
301
+ ```
302
+
303
+ ### isCompositeFiber
304
+
305
+ returns `true` if the fiber is a composite fiber. composite fibers represent class components, function components, memoized components, and so on (anything that can actually render output).
306
+
307
+ ```typescript
308
+ import { isCompositeFiber } from 'bippy';
309
+
310
+ if (isCompositeFiber(fiber)) {
311
+ console.log('fiber is a composite fiber');
312
+ }
313
+ ```
314
+
315
+ ### getDisplayName
316
+
317
+ returns the display name of the fiber's component, falling back to the component's function or class name if available.
318
+
319
+ ```typescript
320
+ import { getDisplayName } from 'bippy';
321
+
322
+ console.log(getDisplayName(fiber));
323
+ ```
324
+
325
+ ### getType
326
+
327
+ returns the underlying type (the component definition) for a given fiber. for example, this could be a function component or class component.
328
+
329
+ ```jsx
330
+ import { getType } from 'bippy';
331
+ import { memo } from 'react';
332
+
333
+ const RealComponent = () => {
334
+ return <div>hello</div>;
335
+ };
336
+ const MemoizedComponent = memo(() => {
337
+ return <div>hello</div>;
338
+ });
339
+
340
+ console.log(getType(fiberForMemoizedComponent) === RealComponent);
341
+ ```
342
+
343
+ ### getNearestHostFiber / getNearestHostFibers
344
+
345
+ getNearestHostFiber returns the closest host fiber above or below a given fiber. getNearestHostFibers(fiber) returns all host fibers associated with the provided fiber and its subtree.
346
+
347
+ ```jsx
348
+ import { getNearestHostFiber, getNearestHostFibers } from 'bippy';
349
+
350
+ // ...
351
+
352
+ function Component() {
353
+ return (
354
+ <>
355
+ <div>hello</div>
356
+ <div>world</div>
357
+ </>
358
+ );
359
+ }
360
+
361
+ console.log(getNearestHostFiber(fiberForComponent)); // <div>hello</div>
362
+ console.log(getNearestHostFibers(fiberForComponent)); // [<div>hello</div>, <div>world</div>]
363
+ ```
364
+
365
+ ### getTimings
366
+
367
+ returns the self and total render times for the fiber.
368
+
369
+ ```typescript
370
+ // timings don't exist in react production builds
371
+ if (fiber.actualDuration !== undefined) {
372
+ const { selfTime, totalTime } = getTimings(fiber);
373
+ console.log(selfTime, totalTime);
374
+ }
375
+ ```
376
+
377
+ ### getFiberStack
378
+
379
+ returns an array representing the stack of fibers from the current fiber up to the root.
380
+
381
+ ```typescript
382
+ [fiber, fiber.return, fiber.return.return, ...]
383
+ ```
384
+
385
+ ### getMutatedHostFibers
386
+
387
+ returns an array of all host fibers that have committed and rendered in the provided fiber's subtree.
388
+
389
+ ```typescript
390
+ import { getMutatedHostFibers } from 'bippy';
391
+
392
+ console.log(getMutatedHostFibers(fiber));
393
+ ```
394
+
395
+ ### isValidFiber
396
+
397
+ returns `true` if the given object is a valid React Fiber (i.e., has a tag, stateNode, return, child, sibling, etc.).
398
+
399
+ ```typescript
400
+ import { isValidFiber } from 'bippy';
401
+
402
+ console.log(isValidFiber(fiber));
403
+ ```
105
404
 
106
405
  ## examples
107
406
 
@@ -161,22 +460,22 @@ const visit = createFiberVisitor({
161
460
  });
162
461
 
163
462
  /**
164
- * `instrument` is a function that installs the React DevTools global
165
- * hook and allows you to set up custom handlers for React fiber events.
463
+ * `instrument` is a function that installs the react DevTools global
464
+ * hook and allows you to set up custom handlers for react fiber events.
166
465
  */
167
466
  instrument(
168
467
  /**
169
468
  * `secure` is a function that wraps your handlers in a try/catch
170
469
  * and prevents it from crashing the app. it also prevents it from
171
- * running on unsupported React versions and during production.
470
+ * running on unsupported react versions and during production.
172
471
  *
173
472
  * this is not required but highly recommended to provide "safeguards"
174
473
  * in case something breaks.
175
474
  */
176
475
  secure({
177
476
  /**
178
- * `onCommitFiberRoot` is a handler that is called when React is
179
- * ready to commit a fiber root. this means that React is has
477
+ * `onCommitFiberRoot` is a handler that is called when react is
478
+ * ready to commit a fiber root. this means that react is has
180
479
  * rendered your entire app and is ready to apply changes to
181
480
  * the host tree (e.g. via DOM mutations).
182
481
  */
@@ -286,6 +585,44 @@ instrument(
286
585
  );
287
586
  ```
288
587
 
588
+ ## glossary
589
+
590
+ - fiber: a "unit of execution" in react, representing a component or dom element
591
+ - commit: the process of applying changes to the host tree (e.g. DOM mutations)
592
+ - render: the process of building the fiber tree by executing component function/classes
593
+ - host tree: the tree of UI elements that react mutates (e.g. DOM elements)
594
+ - reconciler (or "renderer"): custom bindings for react, e.g. react-dom, react-native, react-three-fiber, etc to mutate the host tree
595
+ - `rendererID`: the id of the reconciler, starting at 1 (can be from multiple reconciler instances)
596
+ - `root`: a special `FiberRoot` type that contains the container fiber (the one you pass to `ReactDOM.createRoot`) in the `current` property
597
+ - `onCommitFiberRoot`: called when react is ready to commit a fiber root
598
+ - `onPostCommitFiberRoot`: called when react has committed a fiber root and effects have run
599
+ - `onCommitFiberUnmount`: called when a fiber unmounts
600
+
601
+ ## development
602
+
603
+ we use a pnpm monorepo, get started by running:
604
+
605
+ ```shell
606
+ pnpm install
607
+ # create dev builds
608
+ pnpm run dev
609
+ # run unit tests
610
+ pnpm run test
611
+ ```
612
+
613
+ you can ad-hoc test by running `pnpm run dev` in the `/kitchen-sink` directory.
614
+
615
+ ```shell
616
+ cd kitchen-sink
617
+ pnpm run dev
618
+ ```
619
+
289
620
  ## misc
290
621
 
622
+ we use this project internally in [react-scan](https://github.com/aidenybai/react-scan), which is deployed with proper safeguards to ensure it's only used in development or error-guarded in production.
623
+
624
+ while i maintain this specifically for react-scan, those seeking more robust solutions might consider [its-fine](https://github.com/pmndrs/its-fine) for accessing fibers within react using hooks, or [react-devtools-inline](https://www.npmjs.com/package/react-devtools-inline) for a headful interface.
625
+
626
+ if you plan to use this project beyond experimentation, please review [react-scan's source code](https://github.com/aidenybai/react-scan) to understand our safeguarding practices.
627
+
291
628
  the original bippy character is owned and created by [@dairyfreerice](https://www.instagram.com/dairyfreerice). this project is not related to the bippy brand, i just think the character is cute.