fix(mobile): resolve user avatars, chat alignment, timestamp formatting, and payslips API

This commit is contained in:
2026-08-10 17:55:14 +08:00
parent 6fe1fe125e
commit 9597cbf99a
1468 changed files with 172818 additions and 65664 deletions

1261
node_modules/bippy/src/core.ts generated vendored Normal file

File diff suppressed because it is too large Load Diff

3
node_modules/bippy/src/index.ts generated vendored Normal file
View File

@@ -0,0 +1,3 @@
import "./install-hook-only.js";
export * from "./core.js";

3
node_modules/bippy/src/install-hook-only.ts generated vendored Normal file
View File

@@ -0,0 +1,3 @@
import { safelyInstallRDTHook } from "./rdt-hook.js";
safelyInstallRDTHook();

270
node_modules/bippy/src/rdt-hook.ts generated vendored Normal file
View File

@@ -0,0 +1,270 @@
// IMPORTANT:
// this file is super important to load the __REACT_DEVTOOLS_GLOBAL_HOOK__ object
// without this, we can't stub the React DevTools global hook, we don't have a way to instrument the application
// make sure you import this file first before anything else (particularly React)
import type { ReactDevToolsGlobalHook, ReactRenderer } from "./types.js";
import { toUnsubscribe, type Unsubscribe } from "./unsubscribe.js";
export const version = process.env.VERSION;
export const BIPPY_INSTRUMENTATION_STRING = `bippy-${version}`;
const objectDefineProperty = Object.defineProperty;
// eslint-disable-next-line @typescript-eslint/unbound-method
const objectHasOwnProperty = Object.prototype.hasOwnProperty;
const NO_OP = () => {
/**/
};
const checkDCE = (functionToCheck: unknown): void => {
try {
const code = Function.prototype.toString.call(functionToCheck);
if (code.indexOf("^_^") > -1) {
setTimeout(() => {
throw new Error(
"React is running in production mode, but dead code " +
"elimination has not been applied. Read how to correctly " +
"configure React for production: " +
"https://reactjs.org/link/perf-use-production-build",
);
});
}
} catch {}
};
export const isRealReactDevtools = (
rdtHook: ReactDevToolsGlobalHook | undefined | null = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__,
): boolean => {
return Boolean(rdtHook && "getFiberRoots" in rdtHook);
};
let isReactRefreshOverride = false;
let injectFnStr: string | undefined = undefined;
export const isReactRefresh = (
rdtHook: ReactDevToolsGlobalHook | undefined | null = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__,
): boolean => {
if (isReactRefreshOverride) return true;
if (rdtHook && typeof rdtHook.inject === "function") {
injectFnStr = rdtHook.inject.toString();
}
// https://github.com/facebook/react/blob/8f8b336734d7c807f5aa11b0f31540e63302d789/packages/react-refresh/src/ReactFreshRuntime.js#L459
return Boolean(injectFnStr?.includes("(injected)"));
};
export const _onActiveListeners = new Set<() => unknown>();
export const _renderers = new Set<ReactRenderer>();
const rendererInjectListeners = new Set<(renderer: ReactRenderer) => void>();
// re-wrapping inject (e.g. after the hook is replaced) leaves the old
// wrapper in the call chain, so notifications are deduped per renderer
const notifiedRenderers = new WeakSet<ReactRenderer>();
let notifyingInject: ReactDevToolsGlobalHook["inject"] | null = null;
const ensureInjectNotifiesListeners = (rdtHook: ReactDevToolsGlobalHook): void => {
if (rdtHook.inject === notifyingInject) return;
const prevInject = rdtHook.inject;
const nextInject = (renderer: ReactRenderer) => {
const rendererId = prevInject.call(rdtHook, renderer);
if (!notifiedRenderers.has(renderer)) {
notifiedRenderers.add(renderer);
for (const listener of rendererInjectListeners) {
listener(renderer);
}
}
return rendererId;
};
rdtHook.inject = nextInject;
notifyingInject = nextInject;
};
/**
* Subscribes to future renderer injections into the DevTools hook. The
* single shared inject wrapper lets multiple consumers (override methods,
* react-refresh, user code) observe renderers without stacking patches
* whose restore order matters. Returns an unsubscribe function.
*/
export const onRendererInject = (listener: (renderer: ReactRenderer) => void): Unsubscribe => {
ensureInjectNotifiesListeners(getRDTHook());
rendererInjectListeners.add(listener);
return toUnsubscribe(() => {
rendererInjectListeners.delete(listener);
});
};
export const installRDTHook = (onActive?: () => unknown): ReactDevToolsGlobalHook => {
if (onActive) {
_onActiveListeners.add(onActive);
}
const renderers = new Map<number, ReactRenderer>();
let rendererIdCounter = 0;
let rdtHook: ReactDevToolsGlobalHook = {
_instrumentationIsActive: false,
_instrumentationSource: BIPPY_INSTRUMENTATION_STRING,
checkDCE,
hasUnsupportedRendererAttached: false,
inject(renderer) {
const nextRendererId = ++rendererIdCounter;
renderers.set(nextRendererId, renderer);
_renderers.add(renderer);
if (!rdtHook._instrumentationIsActive) {
rdtHook._instrumentationIsActive = true;
_onActiveListeners.forEach((listener) => listener());
}
return nextRendererId;
},
on: NO_OP,
onCommitFiberRoot: NO_OP,
onCommitFiberUnmount: NO_OP,
onPostCommitFiberRoot: NO_OP,
renderers,
supportsFiber: true,
supportsFlight: true,
};
try {
objectDefineProperty(globalThis, "__REACT_DEVTOOLS_GLOBAL_HOOK__", {
configurable: true,
enumerable: true,
get() {
return rdtHook;
},
set(newHook) {
if (newHook && typeof newHook === "object") {
const ourRenderers = rdtHook.renderers;
rdtHook = newHook;
if (ourRenderers.size > 0) {
ourRenderers.forEach((renderer, id) => {
_renderers.add(renderer);
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
newHook.renderers.set(id, renderer);
});
patchRDTHook(onActive);
}
}
},
});
// [!] this is a hack for chrome extensions - if we install before React DevTools, we could accidently prevent React DevTools from installing:
// https://github.com/facebook/react/blob/18eaf51bd51fed8dfed661d64c306759101d0bfd/packages/react-devtools-extensions/src/contentScripts/installHook.js#L30C6-L30C27
// eslint-disable-next-line @typescript-eslint/unbound-method
const originalWindowHasOwnProperty = window.hasOwnProperty;
let hasRanHack = false;
objectDefineProperty(window, "hasOwnProperty", {
configurable: true,
value: function (this: unknown, ...args: [PropertyKey]) {
try {
if (!hasRanHack && args[0] === "__REACT_DEVTOOLS_GLOBAL_HOOK__") {
globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__ = undefined;
// special falsy value to know that we've already installed before
hasRanHack = true;
return -0;
}
} catch {}
return originalWindowHasOwnProperty.apply(this, args);
},
writable: true,
});
} catch {
patchRDTHook(onActive);
}
return rdtHook;
};
export const patchRDTHook = (onActive?: () => unknown): void => {
if (onActive) {
_onActiveListeners.add(onActive);
}
try {
const rdtHook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
if (!rdtHook) return;
if (!rdtHook._instrumentationSource) {
rdtHook.checkDCE = checkDCE;
rdtHook.supportsFiber = true;
rdtHook.supportsFlight = true;
rdtHook.hasUnsupportedRendererAttached = false;
rdtHook._instrumentationSource = BIPPY_INSTRUMENTATION_STRING;
rdtHook._instrumentationIsActive = false;
// we need to be careful here (needs to be below _instrumentationSource) else it causes excessive recursion
const isReactDevtools = isRealReactDevtools(rdtHook);
if (!isReactDevtools) {
rdtHook.on = NO_OP;
}
if (rdtHook.renderers.size) {
rdtHook._instrumentationIsActive = true;
_onActiveListeners.forEach((listener) => listener());
return;
}
const prevInject = rdtHook.inject;
const isRefresh = isReactRefresh(rdtHook);
if (isRefresh && !isReactDevtools) {
isReactRefreshOverride = true;
// but since the underlying implementation doens't care,
// it's ok: https://github.com/facebook/react/blob/18eaf51bd51fed8dfed661d64c306759101d0bfd/packages/react-refresh/src/ReactFreshRuntime.js#L430
const injectedRendererId = rdtHook.inject({
scheduleRefresh() {},
} as unknown as ReactRenderer);
if (injectedRendererId) {
rdtHook._instrumentationIsActive = true;
}
}
rdtHook.inject = (renderer) => {
const rendererId = prevInject(renderer);
_renderers.add(renderer);
if (isRefresh) {
// react refresh doesn't inject this properly
// https://github.com/facebook/react/blob/18eaf51bd51fed8dfed661d64c306759101d0bfd/packages/react-refresh/src/ReactFreshRuntime.js#L430
rdtHook.renderers.set(rendererId, renderer);
}
rdtHook._instrumentationIsActive = true;
_onActiveListeners.forEach((listener) => listener());
return rendererId;
};
}
if (
rdtHook.renderers.size ||
rdtHook._instrumentationIsActive ||
// depending on this to inject is unsafe, since inject could occur before and we wouldn't know
isReactRefresh()
) {
onActive?.();
}
} catch {}
};
export const hasRDTHook = (): boolean => {
return objectHasOwnProperty.call(globalThis, "__REACT_DEVTOOLS_GLOBAL_HOOK__");
};
/**
* Returns the current React DevTools global hook.
*/
export const getRDTHook = (onActive?: () => unknown): ReactDevToolsGlobalHook => {
if (!hasRDTHook()) {
return installRDTHook(onActive);
}
patchRDTHook(onActive);
// must exist at this point
return globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__ as ReactDevToolsGlobalHook;
};
export const isClientEnvironment = (): boolean => {
return Boolean(
typeof window !== "undefined" &&
// eslint-disable-next-line @typescript-eslint/unbound-method
(window.document?.createElement || window.navigator?.product === "ReactNative"),
);
};
/**
* Usually used purely for side effect
*/
export const safelyInstallRDTHook = () => {
try {
// __REACT_DEVTOOLS_GLOBAL_HOOK__ must exist before React is ever executed
if (isClientEnvironment()) {
getRDTHook();
}
} catch {}
};

9
node_modules/bippy/src/react-refresh/constants.ts generated vendored Normal file
View File

@@ -0,0 +1,9 @@
export const HMR_RECONNECT_DELAY_MS = 1000;
export const PENDING_HOT_UPDATE_MAX_AGE_MS = 10_000;
export const VITE_WS_TOKEN_REGEX = /wsToken = "([^"]+)"/;
export const HMR_SOURCE_FILE_EXTENSION_REGEX = /\.(?:tsx|ts|jsx|js|mjs|cjs|css)$/;
export const BUNDLER_LAYER_PREFIX_REGEX = /^(?:\.\/)?\/?\([a-z][a-z0-9-]*\)\//;

View File

@@ -0,0 +1,33 @@
import { isClientEnvironment } from "../rdt-hook.js";
import { createMetroHmrTransport } from "./metro-hmr-transport.js";
import { createNextWebpackHmrTransport } from "./next-webpack-hmr-transport.js";
import { HmrTransport, HmrUpdateHandler } from "./types.js";
import { createViteHmrTransport } from "./vite-hmr-transport.js";
/**
* Detects the dev server's HMR transport (Next.js webpack, then Metro for
* React Native, then Vite) and subscribes `onHmrUpdate` to hot updates.
* Resolves `null` on the server (SSR) and when no known transport is
* available (production builds, unsupported bundlers — Turbopack exposes
* `window.TURBOPACK_CHUNK_UPDATE_LISTENERS` but its update payload shape
* has not been validated, so it is not wired up yet).
*
* @example
* ```ts
* const transport = await detectHmrTransport((filePaths) => {
* console.log("hot updated:", filePaths);
* });
* transport?.dispose();
* ```
*/
export const detectHmrTransport = async (
onHmrUpdate: HmrUpdateHandler,
): Promise<HmrTransport | null> => {
if (!isClientEnvironment()) return null;
const webpackTransport = createNextWebpackHmrTransport(onHmrUpdate);
if (webpackTransport) return webpackTransport;
const metroTransport = createMetroHmrTransport(onHmrUpdate);
if (metroTransport) return metroTransport;
return createViteHmrTransport(onHmrUpdate);
};

173
node_modules/bippy/src/react-refresh/index.ts generated vendored Normal file
View File

@@ -0,0 +1,173 @@
import { getType, traverseFiber } from "../core.js";
import { getRDTHook, isClientEnvironment, onRendererInject } from "../rdt-hook.js";
import type { Fiber, FiberRoot, ReactRenderer } from "../types.js";
import { toUnsubscribe, type Unsubscribe } from "../unsubscribe.js";
import { PENDING_HOT_UPDATE_MAX_AGE_MS } from "./constants.js";
import { detectHmrTransport } from "./detect-hmr-transport.js";
import { HmrTransport } from "./types.js";
export interface ReactRefreshUpdate {
/**
* hot-updated source file paths reported by the bundler's HMR transport
* (auto-detected: Next.js webpack, Metro, Vite). Best-effort: empty when
* the bundler does not expose a transport (e.g. Turbopack) or when the
* transport message has not arrived yet (e.g. Metro delivers updates on
* an independent socket).
*/
filePaths: string[];
root: FiberRoot;
/** new component types that were remounted, losing state */
staleComponents: unknown[];
/** mounted fibers whose component types were remounted */
staleFibers: Fiber[];
/** new component types that re-rendered preserving state */
updatedComponents: unknown[];
/** mounted fibers whose component types re-rendered preserving state */
updatedFibers: Fiber[];
}
export interface ReactRefreshHandler {
(update: ReactRefreshUpdate): void;
}
export interface ReactRefreshInstrumentationOptions {
onRefresh?: ReactRefreshHandler;
}
const collectFibersByComponentType = (root: FiberRoot, componentTypes: Set<unknown>): Fiber[] => {
if (componentTypes.size === 0 || !root.current) return [];
const matchedFibers: Fiber[] = [];
traverseFiber(root.current, (fiber) => {
// memo/forwardRef fibers carry the wrapper as fiber.type, while the
// refresh families can register either the wrapper or the inner type
if (componentTypes.has(fiber.type) || componentTypes.has(getType(fiber.type))) {
matchedFibers.push(fiber);
}
});
return matchedFibers;
};
const refreshHandlers = new Set<ReactRefreshHandler>();
const refreshWrappedRenderers = new WeakSet<ReactRenderer>();
let pendingFilePaths: string[] = [];
let pendingFilePathsReceivedAtMs = 0;
const bufferHotUpdateFilePaths = (filePaths: string[]): void => {
const nowMs = Date.now();
if (nowMs - pendingFilePathsReceivedAtMs > PENDING_HOT_UPDATE_MAX_AGE_MS) {
pendingFilePaths = [];
}
pendingFilePaths.push(...filePaths);
pendingFilePathsReceivedAtMs = nowMs;
};
const takeFreshFilePaths = (): string[] => {
if (Date.now() - pendingFilePathsReceivedAtMs > PENDING_HOT_UPDATE_MAX_AGE_MS) return [];
const freshFilePaths = [...pendingFilePaths];
// performReactRefresh calls scheduleRefresh synchronously once per
// mounted root, so clear the pending paths only after the whole
// refresh pass instead of on the first root
queueMicrotask(() => {
pendingFilePaths = [];
});
return freshFilePaths;
};
const wrapRendererScheduleRefresh = (renderer: ReactRenderer): void => {
if (refreshWrappedRenderers.has(renderer)) return;
const originalScheduleRefresh = renderer.scheduleRefresh;
if (typeof originalScheduleRefresh !== "function") return;
refreshWrappedRenderers.add(renderer);
renderer.scheduleRefresh = (root, update) => {
originalScheduleRefresh.call(renderer, root, update);
if (refreshHandlers.size === 0) return;
const staleComponents = Array.from(update.staleFamilies, (family) => family.current);
const updatedComponents = Array.from(update.updatedFamilies, (family) => family.current);
const refreshUpdate: ReactRefreshUpdate = {
filePaths: takeFreshFilePaths(),
root,
staleComponents,
staleFibers: collectFibersByComponentType(root, new Set(staleComponents)),
updatedComponents,
updatedFibers: collectFibersByComponentType(root, new Set(updatedComponents)),
};
for (const handler of refreshHandlers) {
handler(refreshUpdate);
}
};
};
let isRefreshWired = false;
const ensureRefreshWired = (): void => {
if (isRefreshWired) return;
isRefreshWired = true;
const rdtHook = getRDTHook();
for (const renderer of rdtHook.renderers.values()) {
wrapRendererScheduleRefresh(renderer);
}
onRendererInject(wrapRendererScheduleRefresh);
};
let activeTransport: HmrTransport | null = null;
// the bundler's HMR global may not exist yet when an early subscriber
// arrives, so detection is retried whenever the first subscriber (re)appears
const detectTransportForNewSubscriber = (): void => {
void detectHmrTransport(bufferHotUpdateFilePaths).then((detectedTransport) => {
if (!detectedTransport) return;
activeTransport?.dispose();
activeTransport = detectedTransport;
});
};
/**
* Subscribes to react-refresh (fast refresh) updates by wrapping
* `scheduleRefresh` on every renderer injected into the React DevTools
* global hook. The react-refresh runtime calls `scheduleRefresh` after each
* hot update, so this works with any bundler that uses react-refresh (Vite,
* Next.js webpack, Next.js Turbopack, Metro) without bundler-specific code.
* Returns an unsubscribe function (a no-op in non-client environments like
* SSR, so callers never need an environment check). The returned function
* is also a `Disposable`, so it composes with other bippy subscriptions
* through `using`.
*
* The bundler's HMR transport is auto-detected and each refresh update is
* augmented with the hot-updated source file paths it reported.
*
* The handler runs after React has re-rendered with the new component
* types, so the refreshed root's fiber tree already carries them;
* `updatedFibers`/`staleFibers` are the mounted fibers matching the
* hot-swapped component types.
*
* @example
* ```ts
* const unsubscribe = instrumentReactRefresh({
* onRefresh(update) {
* for (const fiber of update.updatedFibers) {
* console.log("hot updated:", getDisplayName(fiber.type));
* }
* console.log("changed files:", update.filePaths);
* },
* });
* unsubscribe();
* ```
*
* Pair with `getSource(fiber)` from `bippy/source` to symbolicate the
* source locations of `updatedFibers` when needed.
*/
export const instrumentReactRefresh = (
options: ReactRefreshInstrumentationOptions,
): Unsubscribe => {
const { onRefresh } = options;
if (!onRefresh || !isClientEnvironment()) return toUnsubscribe(() => {});
ensureRefreshWired();
if (refreshHandlers.size === 0) {
detectTransportForNewSubscriber();
}
refreshHandlers.add(onRefresh);
return toUnsubscribe(() => {
refreshHandlers.delete(onRefresh);
});
};

View File

@@ -0,0 +1,188 @@
import { HMR_RECONNECT_DELAY_MS } from "./constants.js";
import { HmrTransport, HmrUpdateHandler } from "./types.js";
declare global {
var __turboModuleProxy: ((moduleName: string) => unknown) | undefined;
var nativeModuleProxy: Record<string, unknown> | undefined;
}
const getScriptUrlFromSourceCodeModule = (sourceCodeModule: unknown): string | null => {
if (typeof sourceCodeModule !== "object" || sourceCodeModule === null) return null;
if (!("getConstants" in sourceCodeModule)) return null;
const getConstants = sourceCodeModule.getConstants;
if (typeof getConstants !== "function") return null;
let constants: unknown;
try {
constants = getConstants.call(sourceCodeModule);
} catch {
return null;
}
if (typeof constants !== "object" || constants === null) return null;
if (!("scriptURL" in constants)) return null;
const scriptUrl = constants.scriptURL;
return typeof scriptUrl === "string" ? scriptUrl : null;
};
/**
* Resolves the URL the running React Native bundle was loaded from, using
* the same `SourceCode` native module React Native's own dev tooling reads
* (via the TurboModule proxy globals, so react-native is not imported).
* Returns `null` outside a Metro-served React Native runtime.
*
* @example
* ```ts
* getMetroBundleUrl();
* // "http://localhost:8081/index.bundle?platform=ios&dev=true"
* ```
*/
export const getMetroBundleUrl = (): string | null => {
if (typeof globalThis.__turboModuleProxy === "function") {
let sourceCodeModule: unknown;
try {
sourceCodeModule = globalThis.__turboModuleProxy("SourceCode");
} catch {
sourceCodeModule = null;
}
const scriptUrl = getScriptUrlFromSourceCodeModule(sourceCodeModule);
if (scriptUrl) return scriptUrl;
}
const legacySourceCodeModule = globalThis.nativeModuleProxy?.SourceCode;
return getScriptUrlFromSourceCodeModule(legacySourceCodeModule);
};
// HACK: module sourceURLs may be JSC-safe URLs where "//&" stands in for
// "?" (iOS 16.4 stack traces strip query strings), so the query separator
// must be normalized before URL parsing.
const normalizeJscSafeUrl = (jscSafeUrl: string): string => jscSafeUrl.replace("//&", "?");
const getSourcePathFromSourceUrl = (sourceUrl: string): string | null => {
let parsedUrl: URL;
try {
parsedUrl = new URL(normalizeJscSafeUrl(sourceUrl));
} catch {
return null;
}
let sourcePath = decodeURIComponent(parsedUrl.pathname);
if (sourcePath.startsWith("/")) sourcePath = sourcePath.slice(1);
// Metro rewrites each module's real extension to ".bundle" when building
// hot-update sourceURLs, so the original extension is unrecoverable.
if (sourcePath.endsWith(".bundle")) sourcePath = sourcePath.slice(0, -".bundle".length);
return sourcePath.length > 0 ? sourcePath : null;
};
const collectModuleSourcePaths = (hmrModules: unknown, filePaths: string[]) => {
if (!Array.isArray(hmrModules)) return;
for (const hmrModule of hmrModules) {
if (typeof hmrModule !== "object" || hmrModule === null) continue;
if (!("sourceURL" in hmrModule) || typeof hmrModule.sourceURL !== "string") continue;
const sourcePath = getSourcePathFromSourceUrl(hmrModule.sourceURL);
if (!sourcePath || sourcePath.includes("node_modules")) continue;
filePaths.push(sourcePath);
}
};
/**
* Extracts the updated source file paths from a raw Metro HMR WebSocket
* message. Paths are project-relative but extension-less (`src/app`, not
* `src/app.tsx`) because Metro rewrites module extensions to `.bundle`.
* The initial update replayed on connect is skipped. Returns an empty
* array for any other message shape.
*
* @example
* ```ts
* parseMetroUpdatePaths(rawMessageData);
* // ["src/app"]
* ```
*/
export const parseMetroUpdatePaths = (rawMessageData: string): string[] => {
let message: unknown;
try {
message = JSON.parse(rawMessageData);
} catch {
return [];
}
if (typeof message !== "object" || message === null) return [];
if (!("type" in message) || message.type !== "update") return [];
if (!("body" in message) || typeof message.body !== "object" || message.body === null) return [];
const updateBody = message.body;
if ("isInitialUpdate" in updateBody && updateBody.isInitialUpdate === true) return [];
const filePaths: string[] = [];
if ("added" in updateBody) collectModuleSourcePaths(updateBody.added, filePaths);
if ("modified" in updateBody) collectModuleSourcePaths(updateBody.modified, filePaths);
return filePaths;
};
export interface MetroHmrTransportOptions {
bundleUrl?: string;
}
/**
* Subscribes to the Metro dev server's `/hot` HMR WebSocket (as a second
* client alongside React Native's own) and invokes `onHmrUpdate` with the
* updated file paths on every hot update. Reconnects automatically when
* the dev server restarts. Returns `null` when no Metro bundle URL can be
* resolved (production builds, non-Metro runtimes).
*
* @example
* ```ts
* const transport = createMetroHmrTransport((filePaths) => {
* console.log("hot updated:", filePaths);
* });
* transport?.dispose();
* ```
*/
export const createMetroHmrTransport = (
onHmrUpdate: HmrUpdateHandler,
options: MetroHmrTransportOptions = {},
): HmrTransport | null => {
if (typeof WebSocket === "undefined") return null;
const bundleUrl = options.bundleUrl ?? getMetroBundleUrl();
if (!bundleUrl) return null;
let hotSocketUrl: string;
try {
const parsedBundleUrl = new URL(bundleUrl);
const socketProtocol = parsedBundleUrl.protocol === "https:" ? "wss" : "ws";
hotSocketUrl = `${socketProtocol}://${parsedBundleUrl.host}/hot`;
} catch {
return null;
}
let isDisposed = false;
let socket: WebSocket | null = null;
let reconnectTimerId: ReturnType<typeof setTimeout> | undefined;
const scheduleReconnect = () => {
if (isDisposed) return;
reconnectTimerId = setTimeout(connect, HMR_RECONNECT_DELAY_MS);
};
const connect = () => {
if (isDisposed) return;
const connectedSocket = new WebSocket(hotSocketUrl);
socket = connectedSocket;
connectedSocket.onopen = () => {
connectedSocket.send(
JSON.stringify({ type: "register-entrypoints", entryPoints: [bundleUrl] }),
);
};
connectedSocket.onmessage = (event) => {
const filePaths = parseMetroUpdatePaths(String(event.data));
if (filePaths.length > 0) onHmrUpdate(filePaths);
};
connectedSocket.onclose = scheduleReconnect;
};
connect();
return {
dispose: () => {
isDisposed = true;
clearTimeout(reconnectTimerId);
if (socket) {
socket.onclose = null;
socket.close();
}
},
};
};

View File

@@ -0,0 +1,72 @@
import { HMR_SOURCE_FILE_EXTENSION_REGEX } from "./constants.js";
import { normalizeHmrFilePath } from "./normalize-hmr-file-path.js";
import { HmrTransport, HmrUpdateHandler } from "./types.js";
interface WebpackHotUpdateGlobal {
(chunkId: unknown, updatedModules: Record<string, unknown> | undefined, runtime: unknown): void;
}
declare global {
interface Window {
webpackHotUpdate_N_E?: WebpackHotUpdateGlobal;
}
}
/**
* Normalizes webpack hot-update module keys into project-relative source
* file paths, dropping node_modules entries and non-source keys (e.g.
* webpack runtime helpers).
*
* @example
* ```ts
* normalizeWebpackModulePaths(["(app-pages-browser)/./app/page.tsx"]);
* // ["app/page.tsx"]
* ```
*/
export const normalizeWebpackModulePaths = (moduleKeys: string[]): string[] => {
const filePaths: string[] = [];
for (const moduleKey of moduleKeys) {
if (moduleKey.includes("node_modules")) continue;
const filePath = normalizeHmrFilePath(moduleKey);
if (!HMR_SOURCE_FILE_EXTENSION_REGEX.test(filePath)) continue;
filePaths.push(filePath);
}
return filePaths;
};
/**
* Subscribes to Next.js webpack hot updates by wrapping the
* `webpackHotUpdate_N_E` global and invokes `onHmrUpdate` with the updated
* file paths. Returns `null` when the page is not a Next.js webpack dev
* build.
*
* @example
* ```ts
* const transport = createNextWebpackHmrTransport((filePaths) => {
* console.log("hot updated:", filePaths);
* });
* transport?.dispose();
* ```
*/
export const createNextWebpackHmrTransport = (
onHmrUpdate: HmrUpdateHandler,
): HmrTransport | null => {
if (typeof window === "undefined") return null;
const originalHotUpdate = window.webpackHotUpdate_N_E;
if (typeof originalHotUpdate !== "function") return null;
const wrappedHotUpdate: WebpackHotUpdateGlobal = (chunkId, updatedModules, runtime) => {
const filePaths = normalizeWebpackModulePaths(Object.keys(updatedModules ?? {}));
if (filePaths.length > 0) onHmrUpdate(filePaths);
originalHotUpdate(chunkId, updatedModules, runtime);
};
window.webpackHotUpdate_N_E = wrappedHotUpdate;
return {
dispose: () => {
if (window.webpackHotUpdate_N_E === wrappedHotUpdate) {
window.webpackHotUpdate_N_E = originalHotUpdate;
}
},
};
};

View File

@@ -0,0 +1,24 @@
import { normalizeFileName } from "../source/get-source.js";
import { BUNDLER_LAYER_PREFIX_REGEX } from "./constants.js";
/**
* Normalizes a bundler module key or HMR update path into a plain,
* project-relative file path. Strips URL schemes (via
* {@link normalizeFileName}), bundler layer prefixes like
* `(app-pages-browser)/`, and leading `./` segments.
*
* @example
* ```ts
* normalizeHmrFilePath("(app-pages-browser)/./app/page.tsx");
* // "app/page.tsx"
* ```
*/
export const normalizeHmrFilePath = (filePath: string): string => {
let normalizedFilePath = normalizeFileName(filePath);
normalizedFilePath = normalizedFilePath.replace(BUNDLER_LAYER_PREFIX_REGEX, "");
if (normalizedFilePath.startsWith("./")) {
normalizedFilePath = normalizedFilePath.slice(2);
}
return normalizedFilePath;
};

7
node_modules/bippy/src/react-refresh/types.ts generated vendored Normal file
View File

@@ -0,0 +1,7 @@
export interface HmrUpdateHandler {
(filePaths: string[]): void;
}
export interface HmrTransport {
dispose: () => void;
}

View File

@@ -0,0 +1,116 @@
import { HMR_RECONNECT_DELAY_MS, VITE_WS_TOKEN_REGEX } from "./constants.js";
import { HmrTransport, HmrUpdateHandler } from "./types.js";
/**
* Extracts the accepted file paths from a raw Vite HMR WebSocket message.
* Only `js-update` entries are kept (a `css-update` swaps a stylesheet link
* without re-running modules). Returns an empty array for any other message
* shape.
*
* @example
* ```ts
* parseViteUpdatePaths(rawMessageData);
* // ["/src/app.tsx"]
* ```
*/
export const parseViteUpdatePaths = (rawMessageData: string): string[] => {
let message: unknown;
try {
message = JSON.parse(rawMessageData);
} catch {
return [];
}
if (typeof message !== "object" || message === null) return [];
if (!("type" in message) || message.type !== "update") return [];
if (!("updates" in message) || !Array.isArray(message.updates)) return [];
const filePaths: string[] = [];
for (const update of message.updates) {
if (typeof update !== "object" || update === null) continue;
if (!("type" in update) || update.type !== "js-update") continue;
if (!("acceptedPath" in update) || typeof update.acceptedPath !== "string") continue;
filePaths.push(update.acceptedPath);
}
return filePaths;
};
// HACK: a standalone script is not a Vite module, so import.meta.hot is
// unavailable; open a second HMR WebSocket using the wsToken scraped from
// the dev server's own /@vite/client source.
const fetchViteWsToken = async (): Promise<string | null> => {
try {
const response = await fetch("/@vite/client");
if (!response.ok) return null;
const clientSource = await response.text();
return VITE_WS_TOKEN_REGEX.exec(clientSource)?.[1] ?? null;
} catch {
return null;
}
};
/**
* Subscribes to the current page's Vite dev server HMR WebSocket and invokes
* `onHmrUpdate` with the updated file paths on every hot update. Reconnects
* automatically when the dev server restarts. Resolves `null` when the page
* is not served by Vite.
*
* @example
* ```ts
* const transport = await createViteHmrTransport((filePaths) => {
* console.log("hot updated:", filePaths);
* });
* transport?.dispose();
* ```
*/
export const createViteHmrTransport = async (
onHmrUpdate: HmrUpdateHandler,
): Promise<HmrTransport | null> => {
if (typeof window === "undefined" || typeof WebSocket === "undefined") return null;
const initialWsToken = await fetchViteWsToken();
if (!initialWsToken) return null;
let isDisposed = false;
let socket: WebSocket | null = null;
let reconnectTimerId: number | undefined;
const scheduleReconnect = () => {
if (isDisposed) return;
reconnectTimerId = window.setTimeout(() => {
void fetchViteWsToken().then((freshWsToken) => {
if (isDisposed) return;
if (freshWsToken) {
connect(freshWsToken);
} else {
scheduleReconnect();
}
});
}, HMR_RECONNECT_DELAY_MS);
};
const connect = (wsToken: string) => {
if (isDisposed) return;
const socketProtocol = location.protocol === "https:" ? "wss" : "ws";
const connectedSocket = new WebSocket(
`${socketProtocol}://${location.host}/?token=${wsToken}`,
"vite-hmr",
);
socket = connectedSocket;
connectedSocket.onmessage = (event) => {
const filePaths = parseViteUpdatePaths(String(event.data));
if (filePaths.length > 0) onHmrUpdate(filePaths);
};
connectedSocket.onclose = scheduleReconnect;
};
connect(initialWsToken);
return {
dispose: () => {
isDisposed = true;
window.clearTimeout(reconnectTimerId);
if (socket) {
socket.onclose = null;
socket.close();
}
},
};
};

39
node_modules/bippy/src/source/constants.ts generated vendored Normal file
View File

@@ -0,0 +1,39 @@
export const SCHEME_REGEX = /^[a-zA-Z][a-zA-Z\d+\-.]*:/;
export const INTERNAL_SCHEME_PREFIXES = [
"rsc://",
"file:///",
"webpack-internal://",
"webpack://",
"node:",
"turbopack://",
"metro://",
"/app-pages-browser/",
"/(app-pages-browser)/",
] as const;
export const ABOUT_REACT_PREFIX = "about://React/";
export const SERVER_COMPONENT_URL_PREFIXES = ["rsc://", ABOUT_REACT_PREFIX] as const;
export const ANONYMOUS_FILE_PATTERNS = ["<anonymous>", "eval", ""] as const;
export const SOURCE_FILE_EXTENSION_REGEX = /\.(jsx|tsx|ts|js)$/;
export const BUNDLED_FILE_PATTERN_REGEX =
/(\.min|bundle|chunk|vendor|vendors|runtime|polyfill|polyfills)\.(js|mjs|cjs)$|(chunk|bundle|vendor|vendors|runtime|polyfill|polyfills|framework|app|main|index)[-_.][A-Za-z0-9_-]{4,}\.(js|mjs|cjs)$|[\da-f]{8,}\.(js|mjs|cjs)$|[-_.][\da-f]{20,}\.(js|mjs|cjs)$|\/dist\/|\/build\/|\/.next\/|\/out\/|\/node_modules\/|\.webpack\.|\.vite\.|\.turbopack\./i;
export const QUERY_PARAMETER_PATTERN_REGEX = /^\?[\w~.-]+(?:=[^&#]*)?(?:&[\w~.-]+(?:=[^&#]*)?)*$/;
export const SERVER_FRAME_MARKER = "(at Server)";
export const SERVER_ENV_PATTERN = /\(at [^)]+\)$/;
export const REACT_STACK_BOTTOM_FRAME_PATTERNS = [
"react_stack_bottom_frame",
"react-stack-bottom-frame",
] as const;
// the first frame of a _debugStack is the JSX factory itself (jsxDEV), never
// user code, matching what react's own captureOwnerStack pops
export const JSX_FACTORY_FRAME_COUNT = 1;

View File

@@ -0,0 +1,93 @@
import { Fiber } from "../types.js";
import { getDisplayName } from "../core.js";
import { getParentStack } from "./owner-stack.js";
import { getSourceFromSourceMap, getSourceMap } from "./symbolication.js";
import { StackFrame } from "./parse-stack.js";
const extractComponentNameFromSource = (
sourceContent: string,
lineNumber: number,
): string | null => {
const lines = sourceContent.split("\n");
const targetLineIndex = lineNumber - 1;
if (targetLineIndex < 0 || targetLineIndex >= lines.length) {
return null;
}
const startLine = Math.max(0, targetLineIndex - 5);
const endLine = Math.min(lines.length, targetLineIndex + 5);
const contextLines = lines.slice(startLine, endLine).join("\n");
const arrowFunctionPattern = /(?:^|export\s+)(?:const|let|var)\s+(\w+)\s*=/m;
const functionPattern = /(?:^|export\s+)function\s+(\w+)/m;
const classPattern = /(?:^|export\s+)class\s+(\w+)/m;
const arrowMatch = contextLines.match(arrowFunctionPattern);
if (arrowMatch?.[1]) {
return arrowMatch[1];
}
const functionMatch = contextLines.match(functionPattern);
if (functionMatch?.[1]) {
return functionMatch[1];
}
const classMatch = contextLines.match(classPattern);
if (classMatch?.[1]) {
return classMatch[1];
}
return null;
};
export const getDisplayNameFromSource = async (
fiber: Fiber,
cache = true,
fetchFn?: (url: string) => Promise<Response>,
): Promise<string | null> => {
const parentStackFrames = await getParentStack(fiber, cache, fetchFn);
const stackFrame = parentStackFrames.filter((innerFrame) => innerFrame.fileName)[0];
if (!stackFrame?.fileName) {
return getDisplayName(fiber.type);
}
const bundleSourceMap = await getSourceMap(stackFrame.fileName, cache, fetchFn);
if (!bundleSourceMap) {
return getDisplayName(fiber.type);
}
let source: StackFrame | null = null;
if (typeof stackFrame.lineNumber === "number" && typeof stackFrame.columnNumber === "number") {
source = getSourceFromSourceMap(
bundleSourceMap,
stackFrame.lineNumber,
stackFrame.columnNumber,
);
}
if (!source?.fileName || !source.lineNumber) {
return getDisplayName(fiber.type);
}
if (!bundleSourceMap.sourcesContent) {
return getDisplayName(fiber.type);
}
const sourceIndex = bundleSourceMap.sources.indexOf(source.fileName);
if (sourceIndex === -1 || !bundleSourceMap.sourcesContent[sourceIndex]) {
return getDisplayName(fiber.type);
}
const sourceContent = bundleSourceMap.sourcesContent[sourceIndex];
const extractedName = extractComponentNameFromSource(sourceContent, source.lineNumber);
if (extractedName) {
return extractedName;
}
return getDisplayName(fiber.type);
};

257
node_modules/bippy/src/source/get-source.ts generated vendored Normal file
View File

@@ -0,0 +1,257 @@
import { Fiber } from "../types.js";
import { FiberSource } from "./types.js";
import {
SCHEME_REGEX,
INTERNAL_SCHEME_PREFIXES,
ABOUT_REACT_PREFIX,
ANONYMOUS_FILE_PATTERNS,
SOURCE_FILE_EXTENSION_REGEX,
BUNDLED_FILE_PATTERN_REGEX,
QUERY_PARAMETER_PATTERN_REGEX,
} from "./constants.js";
import { getDefinitionFrameFromOwnedChild, getParentStack, hasDebugStack } from "./owner-stack.js";
import { parseDebugStack } from "./parse-debug-stack.js";
import { StackFrame } from "./parse-stack.js";
import { symbolicateStack } from "./symbolication.js";
export const hasDebugSource = (
fiber: Fiber,
): fiber is Fiber & {
_debugSource: NonNullable<Fiber["_debugSource"]>;
} => {
const debugSource = fiber._debugSource;
if (!debugSource) {
return false;
}
return (
typeof debugSource === "object" &&
debugSource !== null &&
"fileName" in debugSource &&
typeof debugSource.fileName === "string" &&
"lineNumber" in debugSource &&
typeof debugSource.lineNumber === "number"
);
};
const toFiberSource = (stackFrame: StackFrame): FiberSource | null =>
stackFrame.fileName
? {
fileName: stackFrame.fileName,
lineNumber: stackFrame.lineNumber,
columnNumber: stackFrame.columnNumber,
functionName: stackFrame.functionName,
}
: null;
// the fiber's own _debugStack (react 19) is captured at its JSX creation
// site, so its first user-space frame IS the usage site - no need to
// re-invoke the component like the throwing trick does
const getUsageFrameFromDebugStack = (fiber: Fiber): StackFrame | null => {
if (!hasDebugStack(fiber)) {
return null;
}
const { frames, isTrusted } = parseDebugStack(fiber._debugStack);
if (!isTrusted) {
return null;
}
for (const stackFrame of frames) {
if (stackFrame.fileName) {
return stackFrame;
}
}
return null;
};
/**
* Returns the source of where the component is used. Available only in dev, for composite {@link Fiber}s.
*
* Resolution order:
* 1. `_debugSource` (react <19, requires the JSX source babel transform)
* 2. the fiber's own `_debugStack` (react 19) - the exact JSX creation site
* 3. an owned child's `_debugStack` bottom frame (react 19) - a location
* inside the component's own body; works for components that the throwing
* trick cannot locate (no hooks, no props access)
* 4. the legacy owner-stack path (throwing trick re-invocation)
*
* @example
* ```ts
* function Parent() {
* const data = useData();
* return <Child name={data.name} />; // <-- captures THIS line
* }
*
* function Child({ name }) {
* return <div>{name}</div>;
* }
*
* const source = await getSource(fiber);
* console.log(source.fileName, source.lineNumber);
* ```
*/
export const getSource = async (
fiber: Fiber,
cache = true,
fetchFn?: (url: string) => Promise<Response>,
): Promise<FiberSource | null> => {
if (hasDebugSource(fiber)) {
const debugSource = fiber._debugSource;
return debugSource || null;
}
const debugStackFrame =
getUsageFrameFromDebugStack(fiber) ?? getDefinitionFrameFromOwnedChild(fiber);
if (debugStackFrame) {
const [symbolicatedFrame] = await symbolicateStack([debugStackFrame], cache, fetchFn);
const debugStackSource = toFiberSource(symbolicatedFrame);
if (debugStackSource) {
return debugStackSource;
}
}
const parentStackFrames = await getParentStack(fiber, cache, fetchFn);
for (const stackFrame of parentStackFrames) {
if (stackFrame.fileName) {
return toFiberSource(stackFrame);
}
}
return null;
};
const getPathSegmentCount = (path: string): number => path.split("/").filter(Boolean).length;
const getFirstPathSegment = (path: string): string | null => {
const segments = path.split("/").filter(Boolean);
return segments[0] ?? null;
};
const stripSingleBasePathPrefix = (path: string): string => {
const firstSlashIndex = path.indexOf("/", 1);
if (firstSlashIndex === -1) {
return path;
}
const basePath = path.slice(0, firstSlashIndex);
if (getPathSegmentCount(basePath) !== 1) {
return path;
}
const remainderPath = path.slice(firstSlashIndex);
if (!SOURCE_FILE_EXTENSION_REGEX.test(remainderPath)) {
return path;
}
if (getPathSegmentCount(remainderPath) < 2) {
return path;
}
const firstRemainderSegment = getFirstPathSegment(remainderPath);
if (!firstRemainderSegment) {
return path;
}
if (firstRemainderSegment.startsWith("@")) {
return path;
}
if (firstRemainderSegment.length > 4) {
return path;
}
return remainderPath;
};
export const normalizeFileName = (fileName: string): string => {
if (!fileName) {
return "";
}
if (ANONYMOUS_FILE_PATTERNS.some((pattern) => pattern === fileName)) {
return "";
}
let normalizedFileName = fileName;
const isHttpUrl =
normalizedFileName.startsWith("http://") || normalizedFileName.startsWith("https://");
if (isHttpUrl) {
try {
const parsedUrl = new URL(normalizedFileName);
normalizedFileName = parsedUrl.pathname;
} catch {}
}
if (isHttpUrl) {
normalizedFileName = stripSingleBasePathPrefix(normalizedFileName);
}
if (normalizedFileName.startsWith(ABOUT_REACT_PREFIX)) {
const remainder = normalizedFileName.slice(ABOUT_REACT_PREFIX.length);
const slashIndex = remainder.indexOf("/");
const colonIndex = remainder.indexOf(":");
if (slashIndex !== -1 && (colonIndex === -1 || slashIndex < colonIndex)) {
normalizedFileName = remainder.slice(slashIndex + 1);
} else {
normalizedFileName = remainder;
}
}
let didStripPrefix = true;
while (didStripPrefix) {
didStripPrefix = false;
for (const prefix of INTERNAL_SCHEME_PREFIXES) {
if (normalizedFileName.startsWith(prefix)) {
normalizedFileName = normalizedFileName.slice(prefix.length);
if (prefix === "file:///") {
normalizedFileName = `/${normalizedFileName.replace(/^\/+/, "")}`;
}
didStripPrefix = true;
break;
}
}
}
if (SCHEME_REGEX.test(normalizedFileName)) {
const schemeMatch = normalizedFileName.match(SCHEME_REGEX);
if (schemeMatch) {
normalizedFileName = normalizedFileName.slice(schemeMatch[0].length);
}
}
if (normalizedFileName.startsWith("//")) {
const firstPathSlashIndex = normalizedFileName.indexOf("/", 2);
normalizedFileName =
firstPathSlashIndex === -1 ? "" : normalizedFileName.slice(firstPathSlashIndex);
}
const queryParameterIndex = normalizedFileName.indexOf("?");
if (queryParameterIndex !== -1) {
const potentialQueryParameters = normalizedFileName.slice(queryParameterIndex);
if (QUERY_PARAMETER_PATTERN_REGEX.test(potentialQueryParameters)) {
normalizedFileName = normalizedFileName.slice(0, queryParameterIndex);
}
}
return normalizedFileName;
};
export const isSourceFile = (fileName: string): boolean => {
const normalizedFileName = normalizeFileName(fileName);
if (!normalizedFileName) {
return false;
}
if (!SOURCE_FILE_EXTENSION_REGEX.test(normalizedFileName)) {
return false;
}
if (BUNDLED_FILE_PATTERN_REGEX.test(normalizedFileName)) {
return false;
}
return true;
};

17
node_modules/bippy/src/source/index.ts generated vendored Normal file
View File

@@ -0,0 +1,17 @@
export { formatOwnerStack, getOwnerStack, getParentStack, hasDebugStack } from "./owner-stack.js";
export { getSource, isSourceFile, normalizeFileName } from "./get-source.js";
export {
getSourceFromSourceMap,
getSourceMap,
symbolicateStack,
type DecodedSourceMapSection,
type IndexSourceMap,
type RawSourceMap,
type SourceMap,
type StandardSourceMap,
} from "./symbolication.js";
export type { FiberSource } from "./types.js";
export { parseStack, type ParseOptions, type StackFrame } from "./parse-stack.js";
export { getDisplayNameFromSource } from "./get-display-name-from-source.js";
export { getFiberHooks, type HookSource, type HooksNode, type HooksTree } from "./inspect-hooks.js";
export { parseHookNames, type HookNames } from "./parse-hook-names.js";

882
node_modules/bippy/src/source/inspect-hooks.ts generated vendored Normal file
View File

@@ -0,0 +1,882 @@
import type { Fiber, ContextDependency, MemoizedState, ReactContext } from "../types.js";
import { parseStack, type StackFrame } from "./parse-stack.js";
import { getRDTHook, _renderers } from "../rdt-hook.js";
const REACT_CONTEXT_TYPE = Symbol.for("react.context");
const REACT_MEMO_CACHE_SENTINEL = Symbol.for("react.memo_cache_sentinel");
const FUNCTION_COMPONENT_TAG = 0;
const CONTEXT_PROVIDER_TAG = 10;
const FORWARD_REF_TAG = 11;
const SIMPLE_MEMO_COMPONENT_TAG = 15;
export interface HookSource {
lineNumber: number | null;
columnNumber: number | null;
fileName: string | null;
functionName: string | null;
}
export interface HooksNode {
id: number | null;
isStateEditable: boolean;
name: string;
value: unknown;
subHooks: HooksNode[];
hookSource: HookSource | null;
}
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface HooksTree extends Array<HooksNode> {}
interface HookLogEntry {
displayName: string | null;
primitive: string;
stackError: Error;
value: unknown;
dispatcherHookName: string;
}
interface DispatcherRefContainer {
H?: unknown;
current?: unknown;
[key: string]: unknown;
}
let hookLog: HookLogEntry[] = [];
let primitiveStackCache: Map<string, StackFrame[]> | null = null;
let currentFiber: Fiber | null = null;
let currentHook: MemoizedState | null = null;
let currentContextDependency: ContextDependency<unknown> | null = null;
let currentThenableIndex = 0;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let currentThenableState: any[] | null = null;
const SuspenseException: unknown = new Error(
"Suspense Exception: This is not a real error! It's an implementation detail of `use` to interrupt the current render.",
);
const parseErrorStack = (error: Error): StackFrame[] =>
parseStack(error.stack || "", { includeInElement: false });
const nextHook = (): MemoizedState | null => {
const hook = currentHook;
if (hook !== null) currentHook = hook.next;
return hook;
};
const readContext = <T>(context: ReactContext<T>): T => {
if (currentFiber === null) return context._currentValue;
if (currentContextDependency === null) {
throw new Error("Context reads do not line up with context dependencies.");
}
if (Object.prototype.hasOwnProperty.call(currentContextDependency, "memoizedValue")) {
const value = currentContextDependency.memoizedValue as T;
currentContextDependency = currentContextDependency.next;
return value;
}
return context._currentValue;
};
const getDispatcherRef = (): DispatcherRefContainer | null => {
const rdtHook = getRDTHook();
const allRenderers = [..._renderers, ...rdtHook.renderers.values()];
for (const renderer of allRenderers) {
const ref = renderer.currentDispatcherRef;
if (ref && typeof ref === "object") return ref as DispatcherRefContainer;
}
return null;
};
const getDispatcherFromRef = (ref: DispatcherRefContainer): unknown =>
"H" in ref ? ref.H : ref.current;
const setDispatcherOnRef = (ref: DispatcherRefContainer, dispatcher: unknown): void => {
if ("H" in ref) {
ref.H = dispatcher;
} else {
ref.current = dispatcher;
}
};
const pushHookLogEntry = (
primitive: string,
value: unknown,
dispatcherHookName: string,
displayName: string | null = null,
): void => {
hookLog.push({
displayName,
primitive,
stackError: new Error(),
value,
dispatcherHookName,
});
};
const dispatcherUse = (usable: unknown): unknown => {
if (usable !== null && typeof usable === "object") {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const asThenable = usable as any;
if (typeof asThenable.then === "function") {
const thenable =
currentThenableState !== null && currentThenableIndex < currentThenableState.length
? currentThenableState[currentThenableIndex++]
: asThenable;
switch (thenable.status) {
case "fulfilled": {
pushHookLogEntry("Promise", thenable.value, "Use");
return thenable.value;
}
case "rejected":
throw thenable.reason;
}
pushHookLogEntry("Unresolved", thenable, "Use");
throw SuspenseException;
}
if (asThenable.$$typeof === REACT_CONTEXT_TYPE && "_currentValue" in asThenable) {
const context: ReactContext<unknown> = asThenable;
const value = readContext(context);
pushHookLogEntry("Context (use)", value, "Use", context.displayName || "Context");
return value;
}
}
throw new Error("An unsupported type was passed to use(): " + String(usable));
};
const dispatcherUseContext = (context: ReactContext<unknown>): unknown => {
const value = readContext(context);
pushHookLogEntry("Context", value, "Context", context.displayName || null);
return value;
};
const dispatcherUseState = (initialState: unknown): [unknown, () => void] => {
const hook = nextHook();
const state =
hook !== null
? hook.memoizedState
: typeof initialState === "function"
? (initialState as () => unknown)()
: initialState;
pushHookLogEntry("State", state, "State");
return [state, () => {}];
};
const dispatcherUseReducer = (
_reducer: unknown,
initialArg: unknown,
init?: (arg: unknown) => unknown,
): [unknown, () => void] => {
const hook = nextHook();
const state =
hook !== null ? hook.memoizedState : init !== undefined ? init(initialArg) : initialArg;
pushHookLogEntry("Reducer", state, "Reducer");
return [state, () => {}];
};
const dispatcherUseRef = (initialValue: unknown): { current: unknown } => {
const hook = nextHook();
const ref = hook !== null ? hook.memoizedState : { current: initialValue };
pushHookLogEntry("Ref", (ref as { current: unknown }).current, "Ref");
return ref as { current: unknown };
};
const dispatcherUseCacheRefresh = (): (() => void) => {
const hook = nextHook();
pushHookLogEntry("CacheRefresh", hook !== null ? hook.memoizedState : () => {}, "CacheRefresh");
return () => {};
};
const dispatcherUseLayoutEffect = (create: () => void): void => {
nextHook();
pushHookLogEntry("LayoutEffect", create, "LayoutEffect");
};
const dispatcherUseInsertionEffect = (create: () => unknown): void => {
nextHook();
pushHookLogEntry("InsertionEffect", create, "InsertionEffect");
};
const dispatcherUseEffect = (create: () => void): void => {
nextHook();
pushHookLogEntry("Effect", create, "Effect");
};
const dispatcherUseImperativeHandle = (ref: unknown): void => {
nextHook();
let instance: unknown;
if (ref !== null && typeof ref === "object" && "current" in ref) {
instance = ref.current;
}
pushHookLogEntry("ImperativeHandle", instance, "ImperativeHandle");
};
const dispatcherUseDebugValue = (
value: unknown,
formatterFn?: (value: unknown) => unknown,
): void => {
pushHookLogEntry(
"DebugValue",
typeof formatterFn === "function" ? formatterFn(value) : value,
"DebugValue",
);
};
const dispatcherUseCallback = (callback: unknown): unknown => {
const hook = nextHook();
pushHookLogEntry(
"Callback",
hook !== null ? (hook.memoizedState as unknown[])[0] : callback,
"Callback",
);
return callback;
};
const dispatcherUseMemo = (nextCreate: () => unknown): unknown => {
const hook = nextHook();
const value = hook !== null ? (hook.memoizedState as unknown[])[0] : nextCreate();
pushHookLogEntry("Memo", value, "Memo");
return value;
};
const dispatcherUseSyncExternalStore = (
_subscribe: unknown,
getSnapshot: () => unknown,
): unknown => {
const hook = nextHook();
nextHook();
const value = hook !== null ? hook.memoizedState : getSnapshot();
pushHookLogEntry("SyncExternalStore", value, "SyncExternalStore");
return value;
};
const dispatcherUseTransition = (): [boolean, () => void] => {
const stateHook = nextHook();
nextHook();
const isPending = stateHook !== null ? (stateHook.memoizedState as boolean) : false;
pushHookLogEntry("Transition", isPending, "Transition");
return [isPending, () => {}];
};
const dispatcherUseDeferredValue = (value: unknown): unknown => {
const hook = nextHook();
const previousValue = hook !== null ? hook.memoizedState : value;
pushHookLogEntry("DeferredValue", previousValue, "DeferredValue");
return previousValue;
};
const dispatcherUseId = (): string => {
const hook = nextHook();
const identifier = hook !== null ? (hook.memoizedState as string) : "";
pushHookLogEntry("Id", identifier, "Id");
return identifier;
};
const dispatcherUseMemoCache = (size: number): unknown[] => {
const fiber = currentFiber;
if (fiber === null || fiber === undefined) return [];
const memoCache = (
fiber.updateQueue as { memoCache?: { data: unknown[][]; index: number } } | null
)?.memoCache;
if (memoCache === null || memoCache === undefined) return [];
let memoCacheSlots = memoCache.data[memoCache.index];
if (memoCacheSlots === undefined) {
memoCacheSlots = memoCache.data[memoCache.index] = Array.from(
{ length: size },
() => REACT_MEMO_CACHE_SENTINEL,
);
}
memoCache.index++;
return memoCacheSlots;
};
const dispatcherUseOptimistic = (passthrough: unknown): [unknown, () => void] => {
const hook = nextHook();
const state = hook !== null ? hook.memoizedState : passthrough;
pushHookLogEntry("Optimistic", state, "Optimistic");
return [state, () => {}];
};
const inspectActionStateHook = (
hook: MemoizedState | null,
initialState: unknown,
): { value: unknown; error: unknown } => {
let value: unknown;
let error: unknown = null;
if (hook !== null) {
const actionResult = hook.memoizedState;
if (
typeof actionResult === "object" &&
actionResult !== null &&
"then" in actionResult &&
typeof actionResult.then === "function"
) {
const thenable = actionResult as { status?: string; value?: unknown; reason?: unknown };
switch (thenable.status) {
case "fulfilled":
value = thenable.value;
break;
case "rejected":
error = thenable.reason;
break;
default:
error = SuspenseException;
value = thenable;
}
} else {
value = actionResult;
}
} else {
value = initialState;
}
return { value, error };
};
const createActionStateDispatcher =
(primitive: string) =>
(_action: unknown, initialState: unknown): [unknown, () => void, boolean] => {
const hook = nextHook();
nextHook();
nextHook();
const stackError = new Error();
const { value, error } = inspectActionStateHook(hook, initialState);
hookLog.push({
displayName: null,
primitive,
stackError,
value,
dispatcherHookName: primitive,
});
if (error !== null) throw error;
return [value, () => {}, false];
};
const dispatcherUseActionState = createActionStateDispatcher("ActionState");
const dispatcherUseFormState = createActionStateDispatcher("FormState");
const dispatcherUseHostTransitionStatus = (): unknown => {
// HACK: creating a minimal fake context because useHostTransitionStatus reads from an internal context not available outside React
const status = readContext({ _currentValue: null } as unknown as ReactContext<unknown>);
pushHookLogEntry("HostTransitionStatus", status, "HostTransitionStatus");
return status;
};
const dispatcherUseEffectEvent = (callback: (...args: unknown[]) => unknown): typeof callback => {
nextHook();
pushHookLogEntry("EffectEvent", callback, "EffectEvent");
return callback;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const Dispatcher: Record<string, (...args: any[]) => any> = {
readContext,
use: dispatcherUse,
useCallback: dispatcherUseCallback,
useContext: dispatcherUseContext,
useEffect: dispatcherUseEffect,
useImperativeHandle: dispatcherUseImperativeHandle,
useLayoutEffect: dispatcherUseLayoutEffect,
useInsertionEffect: dispatcherUseInsertionEffect,
useMemo: dispatcherUseMemo,
useReducer: dispatcherUseReducer,
useRef: dispatcherUseRef,
useState: dispatcherUseState,
useDebugValue: dispatcherUseDebugValue,
useDeferredValue: dispatcherUseDeferredValue,
useTransition: dispatcherUseTransition,
useSyncExternalStore: dispatcherUseSyncExternalStore,
useId: dispatcherUseId,
useHostTransitionStatus: dispatcherUseHostTransitionStatus,
useFormState: dispatcherUseFormState,
useActionState: dispatcherUseActionState,
useOptimistic: dispatcherUseOptimistic,
useMemoCache: dispatcherUseMemoCache,
useCacheRefresh: dispatcherUseCacheRefresh,
useEffectEvent: dispatcherUseEffectEvent,
};
const DispatcherProxy =
typeof Proxy === "undefined"
? Dispatcher
: new Proxy(Dispatcher, {
get(target, prop: string) {
if (Object.prototype.hasOwnProperty.call(target, prop)) return target[prop];
const error = new Error("Missing method in Dispatcher: " + prop);
error.name = "ReactDebugToolsUnsupportedHookError";
throw error;
},
});
const getPrimitiveStackCache = (): Map<string, StackFrame[]> => {
if (primitiveStackCache !== null) return primitiveStackCache;
const cache = new Map<string, StackFrame[]>();
let capturedHookLog: HookLogEntry[];
try {
Dispatcher.useContext({ _currentValue: null });
Dispatcher.useState(null);
Dispatcher.useReducer((state: unknown) => state, null);
Dispatcher.useRef(null);
if (typeof Dispatcher.useCacheRefresh === "function") Dispatcher.useCacheRefresh();
Dispatcher.useLayoutEffect(() => {});
Dispatcher.useInsertionEffect(() => {});
Dispatcher.useEffect(() => {});
Dispatcher.useImperativeHandle(undefined, () => null);
Dispatcher.useDebugValue(null);
Dispatcher.useCallback(() => {});
Dispatcher.useTransition();
Dispatcher.useSyncExternalStore(
() => () => {},
() => null,
() => null,
);
Dispatcher.useDeferredValue(null);
Dispatcher.useMemo(() => null);
Dispatcher.useOptimistic(null, (state: unknown) => state);
Dispatcher.useFormState((state: unknown) => state, null);
Dispatcher.useActionState((state: unknown) => state, null);
Dispatcher.useHostTransitionStatus();
if (typeof Dispatcher.useMemoCache === "function") Dispatcher.useMemoCache(0);
if (typeof Dispatcher.use === "function") {
Dispatcher.use({ $$typeof: REACT_CONTEXT_TYPE, _currentValue: null });
Dispatcher.use({ then() {}, status: "fulfilled", value: null });
try {
Dispatcher.use({ then() {} });
} catch {
/* noop */
}
}
Dispatcher.useId();
if (typeof Dispatcher.useEffectEvent === "function") Dispatcher.useEffectEvent(() => {});
} finally {
capturedHookLog = hookLog;
hookLog = [];
}
for (const hook of capturedHookLog) {
cache.set(hook.primitive, parseErrorStack(hook.stackError));
}
primitiveStackCache = cache;
return primitiveStackCache;
};
let mostLikelyAncestorIndex = 0;
const findSharedIndex = (
hookStack: StackFrame[],
rootStack: StackFrame[],
rootIndex: number,
): number => {
// mostLikelyAncestorIndex is cached across inspections, so it can exceed the
// bounds of a later, shorter root stack (e.g. truncated Error.stackTraceLimit)
if (rootIndex >= rootStack.length) return -1;
const source = rootStack[rootIndex].source;
hookSearch: for (let hookIndex = 0; hookIndex < hookStack.length; hookIndex++) {
if (hookStack[hookIndex].source === source) {
for (
let rootOffset = rootIndex + 1, hookOffset = hookIndex + 1;
rootOffset < rootStack.length && hookOffset < hookStack.length;
rootOffset++, hookOffset++
) {
if (hookStack[hookOffset].source !== rootStack[rootOffset].source) continue hookSearch;
}
return hookIndex;
}
}
return -1;
};
const findCommonAncestorIndex = (rootStack: StackFrame[], hookStack: StackFrame[]): number => {
let rootIndex = findSharedIndex(hookStack, rootStack, mostLikelyAncestorIndex);
if (rootIndex !== -1) return rootIndex;
for (
let candidateIndex = 0;
candidateIndex < rootStack.length && candidateIndex < 5;
candidateIndex++
) {
rootIndex = findSharedIndex(hookStack, rootStack, candidateIndex);
if (rootIndex !== -1) {
mostLikelyAncestorIndex = candidateIndex;
return rootIndex;
}
}
return -1;
};
const parseHookName = (functionName: string | undefined): string => {
if (!functionName) return "";
let startIndex = functionName.lastIndexOf("[as ");
if (startIndex !== -1) {
return parseHookName(functionName.slice(startIndex + "[as ".length, -1));
}
startIndex = functionName.lastIndexOf(".");
startIndex = startIndex === -1 ? 0 : startIndex + 1;
if (functionName.slice(startIndex).startsWith("unstable_")) startIndex += "unstable_".length;
if (functionName.slice(startIndex).startsWith("experimental_"))
startIndex += "experimental_".length;
if (functionName.slice(startIndex, startIndex + 3) === "use") {
if (functionName.length - startIndex === 3) return "Use";
startIndex += 3;
}
return functionName.slice(startIndex);
};
const isReactWrapper = (functionName: string | undefined, wrapperName: string): boolean => {
const hookName = parseHookName(functionName);
if (wrapperName === "HostTransitionStatus") {
return hookName === wrapperName || hookName === "FormStatus";
}
return hookName === wrapperName;
};
const findPrimitiveIndex = (hookStack: StackFrame[], hook: HookLogEntry): number => {
const stackCache = getPrimitiveStackCache();
const primitiveStack = stackCache.get(hook.primitive);
if (primitiveStack === undefined) return -1;
for (
let frameIndex = 0;
frameIndex < primitiveStack.length && frameIndex < hookStack.length;
frameIndex++
) {
if (primitiveStack[frameIndex].source !== hookStack[frameIndex].source) {
if (
frameIndex < hookStack.length - 1 &&
isReactWrapper(hookStack[frameIndex].functionName, hook.dispatcherHookName)
) {
frameIndex++;
}
if (
frameIndex < hookStack.length - 1 &&
isReactWrapper(hookStack[frameIndex].functionName, hook.dispatcherHookName)
) {
frameIndex++;
}
return frameIndex;
}
}
return -1;
};
const parseTrimmedStack = (
rootStack: StackFrame[],
hook: HookLogEntry,
): [StackFrame | null, StackFrame[] | null] => {
const hookStack = parseErrorStack(hook.stackError);
const rootIndex = findCommonAncestorIndex(rootStack, hookStack);
const primitiveIndex = findPrimitiveIndex(hookStack, hook);
if (rootIndex === -1 || primitiveIndex === -1 || rootIndex - primitiveIndex < 2) {
if (primitiveIndex === -1) return [null, null];
return [hookStack[primitiveIndex - 1] ?? null, null];
}
return [hookStack[primitiveIndex - 1] ?? null, hookStack.slice(primitiveIndex, rootIndex - 1)];
};
const NON_ID_HOOK_PRIMITIVES = new Set([
"Context",
"Context (use)",
"DebugValue",
"Promise",
"Unresolved",
"HostTransitionStatus",
]);
const buildTree = (rootStack: StackFrame[], capturedHookLog: HookLogEntry[]): HooksTree => {
const rootChildren: HooksNode[] = [];
let previousStack: StackFrame[] | null = null;
let levelChildren = rootChildren;
let nativeHookID = 0;
const childrenStack: HooksNode[][] = [];
for (const hook of capturedHookLog) {
const [primitiveFrame, stack] = parseTrimmedStack(rootStack, hook);
let displayName = hook.displayName;
if (displayName === null && primitiveFrame !== null) {
displayName =
parseHookName(primitiveFrame.functionName) || parseHookName(hook.dispatcherHookName);
}
if (stack !== null) {
let commonSteps = 0;
if (previousStack !== null) {
while (commonSteps < stack.length && commonSteps < previousStack.length) {
const stackSource = stack[stack.length - commonSteps - 1].source;
const previousSource = previousStack[previousStack.length - commonSteps - 1].source;
if (stackSource !== previousSource) break;
commonSteps++;
}
for (let popIndex = previousStack.length - 1; popIndex > commonSteps; popIndex--) {
levelChildren = childrenStack.pop() ?? rootChildren;
}
}
for (let stackIndex = stack.length - commonSteps - 1; stackIndex >= 1; stackIndex--) {
const children: HooksNode[] = [];
const stackFrame = stack[stackIndex];
const levelChild: HooksNode = {
id: null,
isStateEditable: false,
name: parseHookName(stack[stackIndex - 1].functionName),
value: undefined,
subHooks: children,
hookSource: {
lineNumber: stackFrame.lineNumber ?? null,
columnNumber: stackFrame.columnNumber ?? null,
functionName: stackFrame.functionName ?? null,
fileName: stackFrame.fileName ?? null,
},
};
levelChildren.push(levelChild);
childrenStack.push(levelChildren);
levelChildren = children;
}
previousStack = stack;
}
const { primitive } = hook;
const id = NON_ID_HOOK_PRIMITIVES.has(primitive) ? null : nativeHookID++;
const isStateEditable = primitive === "Reducer" || primitive === "State";
const name = displayName || primitive;
const firstStackFrame = stack?.[0];
const hookSource: HookSource = {
lineNumber: firstStackFrame?.lineNumber ?? null,
columnNumber: firstStackFrame?.columnNumber ?? null,
functionName: firstStackFrame?.functionName ?? null,
fileName: firstStackFrame?.fileName ?? null,
};
levelChildren.push({ id, isStateEditable, name, value: hook.value, subHooks: [], hookSource });
}
processDebugValues(rootChildren, null);
return rootChildren;
};
const processDebugValues = (hooksTree: HooksTree, parentHooksNode: HooksNode | null): void => {
const debugValueNodes: HooksNode[] = [];
for (let nodeIndex = 0; nodeIndex < hooksTree.length; nodeIndex++) {
const hooksNode = hooksTree[nodeIndex];
if (hooksNode.name === "DebugValue" && hooksNode.subHooks.length === 0) {
hooksTree.splice(nodeIndex, 1);
nodeIndex--;
debugValueNodes.push(hooksNode);
} else {
processDebugValues(hooksNode.subHooks, hooksNode);
}
}
if (parentHooksNode !== null) {
if (debugValueNodes.length === 1) {
parentHooksNode.value = debugValueNodes[0].value;
} else if (debugValueNodes.length > 1) {
parentHooksNode.value = debugValueNodes.map(({ value }) => value);
}
}
};
const setupContexts = (contextMap: Map<ReactContext<unknown>, unknown>, fiber: Fiber): void => {
let current: Fiber | null = fiber;
while (current) {
if (current.tag === CONTEXT_PROVIDER_TAG) {
let context = current.type as ReactContext<unknown>;
if ("_context" in context && context._context !== undefined) {
context = context._context as ReactContext<unknown>;
}
if (!contextMap.has(context)) {
contextMap.set(context, context._currentValue);
context._currentValue = (current.memoizedProps as { value: unknown }).value;
}
}
current = current.return;
}
};
const restoreContexts = (contextMap: Map<ReactContext<unknown>, unknown>): void => {
contextMap.forEach((value, context) => {
context._currentValue = value;
});
};
const handleRenderFunctionError = (error: unknown): void => {
if (error === SuspenseException) return;
if (error instanceof Error && error.name === "ReactDebugToolsUnsupportedHookError") throw error;
const wrapperError = new Error("Error rendering inspected component", { cause: error });
wrapperError.name = "ReactDebugToolsRenderError";
(wrapperError as { cause: unknown }).cause = error;
throw wrapperError;
};
const resolveDefaultProps = (
Component: unknown,
baseProps: Record<string, unknown>,
): Record<string, unknown> => {
if (
Component &&
typeof Component === "object" &&
"defaultProps" in Component &&
Component.defaultProps
) {
const props = { ...baseProps };
const defaultProps = Component.defaultProps as Record<string, unknown>;
for (const propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
return props;
}
return baseProps;
};
const suppressConsole = (): Record<string, unknown> => {
const originalMethods: Record<string, unknown> = {};
for (const method in console) {
try {
originalMethods[method] = (console as Record<string, unknown>)[method];
(console as Record<string, unknown>)[method] = () => {};
} catch {
/* noop */
}
}
return originalMethods;
};
const restoreConsole = (originalMethods: Record<string, unknown>): void => {
for (const method in originalMethods) {
try {
(console as Record<string, unknown>)[method] = originalMethods[method];
} catch {
/* noop */
}
}
};
const performDispatcherInspection = (
dispatcherRef: DispatcherRefContainer,
renderFn: () => void,
): HooksTree => {
const previousDispatcher = getDispatcherFromRef(dispatcherRef);
setDispatcherOnRef(dispatcherRef, DispatcherProxy);
let capturedHookLog: HookLogEntry[] = [];
let ancestorStackError: Error | undefined;
try {
ancestorStackError = new Error();
renderFn();
} catch (renderError) {
handleRenderFunctionError(renderError);
} finally {
capturedHookLog = hookLog;
hookLog = [];
setDispatcherOnRef(dispatcherRef, previousDispatcher);
}
const rootStack = ancestorStackError !== undefined ? parseErrorStack(ancestorStackError) : [];
return buildTree(rootStack, capturedHookLog);
};
const requireDispatcherRef = (): DispatcherRefContainer => {
const dispatcherRef = getDispatcherRef();
if (!dispatcherRef) {
throw new Error(
"No React renderer found. Make sure React is loaded and bippy's hook is installed.",
);
}
return dispatcherRef;
};
const resolveContextDependency = (fiber: Fiber): void => {
if (Object.prototype.hasOwnProperty.call(fiber, "dependencies")) {
const dependencies = fiber.dependencies;
currentContextDependency = dependencies !== null ? dependencies.firstContext : null;
} else if (Object.prototype.hasOwnProperty.call(fiber, "dependencies_old")) {
const dependencies = (fiber as unknown as { dependencies_old: typeof fiber.dependencies })
.dependencies_old;
currentContextDependency = dependencies !== null ? dependencies!.firstContext : null;
} else if (Object.prototype.hasOwnProperty.call(fiber, "dependencies_new")) {
const dependencies = (fiber as unknown as { dependencies_new: typeof fiber.dependencies })
.dependencies_new;
currentContextDependency = dependencies !== null ? dependencies!.firstContext : null;
} else if (Object.prototype.hasOwnProperty.call(fiber, "contextDependencies")) {
const contextDependencies = (
fiber as unknown as {
contextDependencies: { first: ContextDependency<unknown> | null } | null;
}
).contextDependencies;
currentContextDependency = contextDependencies !== null ? contextDependencies.first : null;
} else {
throw new Error("Unsupported React version.");
}
};
export const getFiberHooks = (fiber: Fiber): HooksTree => {
const dispatcherRef = requireDispatcherRef();
if (
fiber.tag !== FUNCTION_COMPONENT_TAG &&
fiber.tag !== SIMPLE_MEMO_COMPONENT_TAG &&
fiber.tag !== FORWARD_REF_TAG
) {
throw new Error("Unknown Fiber. Needs to be a function component to inspect hooks.");
}
getPrimitiveStackCache();
currentHook = fiber.memoizedState;
currentFiber = fiber;
const debugThenableState =
fiber.dependencies &&
(fiber.dependencies as { _debugThenableState?: { thenables?: unknown[] } })._debugThenableState;
const usedThenables = debugThenableState
? debugThenableState.thenables || debugThenableState
: null;
currentThenableState = Array.isArray(usedThenables) ? usedThenables : null;
currentThenableIndex = 0;
resolveContextDependency(fiber);
const type = fiber.type;
let props = fiber.memoizedProps as Record<string, unknown>;
if (type !== fiber.elementType) {
props = resolveDefaultProps(type, props);
}
const originalConsoleMethods = suppressConsole();
const contextMap = new Map<ReactContext<unknown>, unknown>();
try {
if (
currentContextDependency !== null &&
!Object.prototype.hasOwnProperty.call(currentContextDependency, "memoizedValue")
) {
setupContexts(contextMap, fiber);
}
if (fiber.tag === FORWARD_REF_TAG) {
return performDispatcherInspection(dispatcherRef, () => {
(type as { render: (props: Record<string, unknown>, ref: unknown) => unknown }).render(
props,
fiber.ref,
);
});
}
return performDispatcherInspection(dispatcherRef, () => {
(type as (props: Record<string, unknown>) => unknown)(props);
});
} finally {
currentFiber = null;
currentHook = null;
currentContextDependency = null;
currentThenableState = null;
currentThenableIndex = 0;
restoreContexts(contextMap);
restoreConsole(originalConsoleMethods);
}
};

725
node_modules/bippy/src/source/owner-stack.ts generated vendored Normal file
View File

@@ -0,0 +1,725 @@
import {
_renderers,
ActivityComponentTag,
ClassComponentTag,
Fiber,
ForwardRefTag,
FunctionComponentTag,
getRDTHook,
HostComponentTag,
HostHoistableTag,
HostSingletonTag,
LazyComponentTag,
SimpleMemoComponentTag,
SuspenseComponentTag,
SuspenseListComponentTag,
ViewTransitionComponentTag,
getDisplayName,
traverseFiber,
} from "../core.js";
import { ServerComponentInfo } from "../types.js";
import {
SERVER_FRAME_MARKER,
SERVER_ENV_PATTERN,
SERVER_COMPONENT_URL_PREFIXES,
} from "./constants.js";
import { parseDebugStack } from "./parse-debug-stack.js";
import { parseStack, StackFrame } from "./parse-stack.js";
import { symbolicateStack } from "./symbolication.js";
export const hasDebugStack = (
fiber: Fiber,
): fiber is Fiber & {
_debugStack: NonNullable<Fiber["_debugStack"]>;
} => {
return fiber._debugStack instanceof Error && typeof fiber._debugStack?.stack === "string";
};
const isFiberOwner = (owner: Fiber | ServerComponentInfo): owner is Fiber =>
typeof (owner as Fiber).tag === "number";
// react's typings (and bippy's Fiber, which mirrors them) declare _debugOwner
// as a Fiber, but react 19 flight sets a ReactComponentInfo object for server
// component owners
const getDebugOwner = (fiber: Fiber): Fiber | ServerComponentInfo | undefined =>
fiber._debugOwner as Fiber | ServerComponentInfo | undefined;
/**
* Locates a frame inside the fiber's own function body without invoking it:
* any child fiber owned by this fiber was created by JSX inside its body, so
* the bottom user-space frame of that child's _debugStack sits in this
* component. The enclosing line/column (V8 CallSite API) points at the
* function definition start. Requires React 19 (_debugStack).
*/
export const getDefinitionFrameFromOwnedChild = (fiber: Fiber): StackFrame | null => {
let ownedChildDebugStack: Error | null = null;
traverseFiber(fiber, (childFiber) => {
if (childFiber === fiber) {
return false;
}
const childOwner = childFiber._debugOwner;
if (
(childOwner === fiber || (fiber.alternate !== null && childOwner === fiber.alternate)) &&
childFiber._debugStack instanceof Error
) {
ownedChildDebugStack = childFiber._debugStack;
return true;
}
return false;
});
if (!ownedChildDebugStack) {
return null;
}
const { frames, isTrusted } = parseDebugStack(ownedChildDebugStack);
if (!isTrusted) {
return null;
}
for (let frameIndex = frames.length - 1; frameIndex >= 0; frameIndex--) {
const stackFrame = frames[frameIndex];
if (!stackFrame.fileName) {
continue;
}
return {
...stackFrame,
lineNumber: stackFrame.enclosingLineNumber || stackFrame.lineNumber,
columnNumber: stackFrame.enclosingColumnNumber || stackFrame.columnNumber,
};
}
return null;
};
const getCurrentDispatcher = (): null | React.RefObject<unknown> => {
const rdtHook = getRDTHook();
for (const renderer of [...Array.from(_renderers), ...Array.from(rdtHook.renderers.values())]) {
const currentDispatcherRef = renderer.currentDispatcherRef;
if (currentDispatcherRef && typeof currentDispatcherRef === "object") {
return "H" in currentDispatcherRef ? currentDispatcherRef.H : currentDispatcherRef.current;
}
}
return null;
};
const setCurrentDispatcher = (value: null | React.RefObject<unknown>): void => {
for (const renderer of _renderers) {
const currentDispatcherRef = renderer.currentDispatcherRef;
if (currentDispatcherRef && typeof currentDispatcherRef === "object") {
if ("H" in currentDispatcherRef) {
currentDispatcherRef.H = value;
} else {
currentDispatcherRef.current = value;
}
}
}
};
const describeBuiltInComponentFrame = (name: string): string => {
return `\n in ${name}`;
};
export const describeDebugInfoFrame = (name: string, env?: string): string => {
let frameDescription = describeBuiltInComponentFrame(name);
if (env) {
frameDescription += ` (at ${env})`;
}
return frameDescription;
};
let reEntry = false;
// Computing a frame throws and parses two full error stacks, so cache per
// component type like React DevTools does.
const componentFrameCache = new WeakMap<React.ComponentType<unknown>, string>();
// https://github.com/facebook/react/blob/f739642745577a8e4dcb9753836ac3589b9c590a/packages/react-devtools-shared/src/backend/shared/DevToolsComponentStackFrame.js#L22
const describeNativeComponentFrame = (
component: React.ComponentType<unknown>,
construct: boolean,
): string => {
if (!component || reEntry) {
return "";
}
const cachedFrame = componentFrameCache.get(component);
if (cachedFrame !== undefined) {
return cachedFrame;
}
const previousPrepareStackTrace = Error.prepareStackTrace;
// HACK: V8 API allows undefined but bun-types declares it as non-optional
(Error as { prepareStackTrace?: typeof Error.prepareStackTrace }).prepareStackTrace = undefined;
reEntry = true;
const previousDispatcher = getCurrentDispatcher();
setCurrentDispatcher(null);
const previousConsoleError = console.error;
const previousConsoleWarn = console.warn;
console.error = () => {};
console.warn = () => {};
try {
/**
* Finding a common stack frame between sample and control errors can be
* tricky given the different types and levels of stack trace truncation from
* different JS VMs. So instead we'll attempt to control what that common
* frame should be through this object method:
* Having both the sample and control errors be in the function under the
* `DescribeNativeComponentFrameRoot` property, + setting the `name` and
* `displayName` properties of the function ensures that a stack
* frame exists that has the method name `DescribeNativeComponentFrameRoot` in
* it for both control and sample stacks.
*/
const RunInRootFrame = {
DetermineComponentFrameRoot() {
let control: unknown;
try {
// This should throw.
if (construct) {
// Something should be setting the props in the constructor.
const ThrowingConstructor = function () {
throw Error();
};
Object.defineProperty(ThrowingConstructor.prototype, "props", {
set: function () {
// We use a throwing setter instead of frozen or non-writable props
// because that won't throw in a non-strict mode function.
throw Error();
},
});
if (typeof Reflect === "object" && Reflect.construct) {
// We construct a different control for this case to include any extra
// frames added by the construct call.
try {
Reflect.construct(ThrowingConstructor, []);
} catch (caughtError) {
control = caughtError;
}
Reflect.construct(component, [], ThrowingConstructor);
} else {
try {
// @ts-expect-error -- ThrowingConstructor is a constructor function
ThrowingConstructor.call();
} catch (caughtError) {
control = caughtError;
}
// @ts-expect-error -- ThrowingConstructor is a constructor function
component.call(ThrowingConstructor.prototype);
}
} else {
try {
throw Error();
} catch (caughtError) {
control = caughtError;
}
// TODO(luna): This will currently only throw if the function component
// tries to access React/ReactDOM/props. We should probably make this throw
// in simple components too
const maybePromise = (component as () => Promise<unknown>)();
// If the function component returns a promise, it's likely an async
// component, which we don't yet support. Attach a noop catch handler to
// silence the error.
// TODO: Implement component stacks for async client components?
// eslint-disable-next-line @typescript-eslint/no-misused-promises -- we literally check if this is a promise here
if (maybePromise && typeof maybePromise.catch === "function") {
maybePromise.catch(() => {});
}
}
} catch (sample: unknown) {
// This is inlined manually because closure doesn't do it for us.
if (
sample instanceof Error &&
control instanceof Error &&
typeof sample.stack === "string"
) {
return [sample.stack, control.stack];
}
}
return [null, null];
},
};
// @ts-expect-error --- displayName is not a property of the function
RunInRootFrame.DetermineComponentFrameRoot.displayName = "DetermineComponentFrameRoot";
const namePropDescriptor = Object.getOwnPropertyDescriptor(
// eslint-disable-next-line @typescript-eslint/unbound-method
RunInRootFrame.DetermineComponentFrameRoot,
"name",
);
// Before ES6, the `name` property was not configurable.
if (namePropDescriptor?.configurable) {
// V8 utilizes a function's `name` property when generating a stack trace.
Object.defineProperty(
// eslint-disable-next-line @typescript-eslint/unbound-method
RunInRootFrame.DetermineComponentFrameRoot,
// Configurable properties can be updated even if its writable descriptor
// is set to `false`.
"name",
{ value: "DetermineComponentFrameRoot" },
);
}
const [sampleStack, controlStack] = RunInRootFrame.DetermineComponentFrameRoot();
if (sampleStack && controlStack) {
// This extracts the first frame from the sample that isn't also in the control.
// Skipping one frame that we assume is the frame that calls the two.
const sampleLines = sampleStack.split("\n");
const controlLines = controlStack.split("\n");
let sampleIndex = 0;
let controlIndex = 0;
while (
sampleIndex < sampleLines.length &&
!sampleLines[sampleIndex].includes("DetermineComponentFrameRoot")
) {
sampleIndex++;
}
while (
controlIndex < controlLines.length &&
!controlLines[controlIndex].includes("DetermineComponentFrameRoot")
) {
controlIndex++;
}
// We couldn't find our intentionally injected common root frame, attempt
// to find another common root frame by search from the bottom of the
// control stack...
if (sampleIndex === sampleLines.length || controlIndex === controlLines.length) {
sampleIndex = sampleLines.length - 1;
controlIndex = controlLines.length - 1;
while (
sampleIndex >= 1 &&
controlIndex >= 0 &&
sampleLines[sampleIndex] !== controlLines[controlIndex]
) {
// We expect at least one stack frame to be shared.
// Typically this will be the root most one. However, stack frames may be
// cut off due to maximum stack limits. In this case, one maybe cut off
// earlier than the other. We assume that the sample is longer or the same
// and there for cut off earlier. So we should find the root most frame in
// the sample somewhere in the control.
controlIndex--;
}
}
for (; sampleIndex >= 1 && controlIndex >= 0; sampleIndex--, controlIndex--) {
// Next we find the first one that isn't the same which should be the
// frame that called our sample function and the control.
if (sampleLines[sampleIndex] !== controlLines[controlIndex]) {
// In V8, the first line is describing the message but other VMs don't.
// If we're about to return the first line, and the control is also on the same
// line, that's a pretty good indicator that our sample threw at same line as
// the control. I.e. before we entered the sample frame. So we ignore this result.
// This can happen if you passed a class to function component, or non-function.
if (sampleIndex !== 1 || controlIndex !== 1) {
do {
sampleIndex--;
controlIndex--;
// We may still have similar intermediate frames from the construct call.
// The next one that isn't the same should be our match though.
if (controlIndex < 0 || sampleLines[sampleIndex] !== controlLines[controlIndex]) {
// V8 adds a "new" prefix for native classes. Let's remove it to make it prettier.
let stackFrame = `\n${sampleLines[sampleIndex].replace(" at new ", " at ")}`;
const displayName = getDisplayName(component);
// If our component frame is labeled "<anonymous>"
// but we have a user-provided "displayName"
// splice it in to make the stack more readable.
if (displayName && stackFrame.includes("<anonymous>")) {
stackFrame = stackFrame.replace("<anonymous>", displayName);
}
// Return the line we found.
componentFrameCache.set(component, stackFrame);
return stackFrame;
}
} while (sampleIndex >= 1 && controlIndex >= 0);
}
break;
}
}
}
} finally {
reEntry = false;
Error.prepareStackTrace = previousPrepareStackTrace;
setCurrentDispatcher(previousDispatcher);
console.error = previousConsoleError;
console.warn = previousConsoleWarn;
}
const componentName = component ? getDisplayName(component) : "";
const syntheticFrame = componentName ? describeBuiltInComponentFrame(componentName) : "";
componentFrameCache.set(component, syntheticFrame);
return syntheticFrame;
};
// https://github.com/facebook/react/blob/ac3e705a18696168acfcaed39dce0cfaa6be8836/packages/react-reconciler/src/ReactFiberComponentStack.js#L180
export const describeFiber = (fiber: Fiber, childFiber: Fiber | null): string => {
const tag = fiber.tag as number;
let stackFrame = "";
switch (tag) {
case ActivityComponentTag:
stackFrame = describeBuiltInComponentFrame("Activity");
break;
case ClassComponentTag:
stackFrame = describeNativeComponentFrame(fiber.type, true);
break;
case ForwardRefTag:
stackFrame = describeNativeComponentFrame(
(fiber.type as { render: React.ComponentType<unknown> }).render,
false,
);
break;
case FunctionComponentTag:
case SimpleMemoComponentTag:
stackFrame = describeNativeComponentFrame(fiber.type, false);
break;
case HostComponentTag:
case HostHoistableTag:
case HostSingletonTag:
stackFrame = describeBuiltInComponentFrame(fiber.type as string);
break;
case LazyComponentTag:
// TODO: When we support Thenables as component types we should rename this.
stackFrame = describeBuiltInComponentFrame("Lazy");
break;
case SuspenseComponentTag:
if (fiber.child !== childFiber && childFiber !== null) {
// If we came from the second Fiber then we're in the Suspense Fallback.
stackFrame = describeBuiltInComponentFrame("Suspense Fallback");
} else {
stackFrame = describeBuiltInComponentFrame("Suspense");
}
break;
case SuspenseListComponentTag:
stackFrame = describeBuiltInComponentFrame("SuspenseList");
break;
case ViewTransitionComponentTag:
// Note: enableViewTransition feature flag is not available in this codebase,
// so we'll always include ViewTransition
stackFrame = describeBuiltInComponentFrame("ViewTransition");
break;
default:
return "";
}
return stackFrame;
};
/**
* Builds a component-stack string by walking the fiber `return` chain, the
* pre-react-19 substitute for `_debugStack` (which is why the frame locations
* come from re-invocation in {@link describeFiber} rather than a real stack).
*/
export const getFallbackParentStack = (thisFiber: Fiber): string => {
try {
let componentStack = "";
let currentFiber: Fiber | null = thisFiber;
let previousFiber: Fiber | null = null;
do {
componentStack += describeFiber(currentFiber, previousFiber);
// Add any Server Component stack frames in reverse order (dev only).
// Since we don't have __DEV__ in this codebase, we'll check for _debugInfo
const debugInfo = currentFiber._debugInfo;
if (debugInfo && Array.isArray(debugInfo)) {
for (let i = debugInfo.length - 1; i >= 0; i--) {
const debugEntry = debugInfo[i];
if (typeof debugEntry.name === "string") {
componentStack += describeDebugInfoFrame(debugEntry.name, debugEntry.env);
}
}
}
previousFiber = currentFiber;
currentFiber = currentFiber.return;
} while (currentFiber);
return componentStack;
} catch (error) {
if (error instanceof Error) {
return `\nError generating stack: ${error.message}\n${error.stack}`;
}
return "";
}
};
/**
* takes Error.stack and formats it to only the React owner stack
*
* before:
* ```
* Error: react-stack-top-frame
* at fakeJSXCallSite (http://localhost:3000/_next/static/chunks/<chunk-name>._.js:17665:16)
* at TodoItem (rsc://React/Server/file:///path/to/project/.next/server/chunks/ssr/<chunk-name>._.js)
* at react-stack-bottom-frame (http://localhost:3000/_next/static/chunks/<chunk-name>._.js:17984:89)
* ```
*
* after:
* ```
* at TodoItem (rsc://React/Server/file:///path/to/project/.next/server/chunks/ssr/<chunk-name>._.js)
* ```
*
* @see https://github.com/facebook/react/blob/main/packages/react-devtools-shared/src/backend/shared/DevToolsOwnerStack.js#L12
*/
export const formatOwnerStack = (stack: string): string => {
const prevPrepareStackTrace = Error.prepareStackTrace;
// HACK: V8 API allows undefined but bun-types declares it as non-optional
(Error as { prepareStackTrace?: typeof Error.prepareStackTrace }).prepareStackTrace = undefined;
let formattedStack = stack;
if (!formattedStack) {
return "";
}
Error.prepareStackTrace = prevPrepareStackTrace;
if (formattedStack.startsWith("Error: react-stack-top-frame\n")) {
// V8's default formatting prefixes with the error message which we
// don't want/need
formattedStack = formattedStack.slice(29);
}
const firstNewlineIndex = formattedStack.indexOf("\n");
if (firstNewlineIndex !== -1) {
// pop the JSX frame
formattedStack = formattedStack.slice(firstNewlineIndex + 1);
}
let bottomFrameIndex = Math.max(
formattedStack.indexOf("react_stack_bottom_frame"),
formattedStack.indexOf("react-stack-bottom-frame"),
);
if (bottomFrameIndex !== -1) {
bottomFrameIndex = formattedStack.lastIndexOf("\n", bottomFrameIndex);
}
if (bottomFrameIndex !== -1) {
// cut off everything after the bottom frame since it'll be internals.
formattedStack = formattedStack.slice(0, bottomFrameIndex);
} else {
// we didn't find any internal callsite out to user space.
// This means that this was called outside an owner or the owner is fully internal.
// to keep things light we exclude the entire trace in this case.
return "";
}
return formattedStack;
};
interface DebugStackEntry {
componentName: string;
stackFrames: StackFrame[];
}
const isReactServerComponentFrame = (stackFrame: StackFrame): boolean =>
Boolean(
stackFrame.functionName && stackFrame.fileName && isServerComponentUrl(stackFrame.fileName),
);
const areStackFramesEqual = (firstFrame: StackFrame, secondFrame: StackFrame): boolean =>
firstFrame.fileName === secondFrame.fileName &&
firstFrame.lineNumber === secondFrame.lineNumber &&
firstFrame.columnNumber === secondFrame.columnNumber;
const buildFunctionNameToRscFramesMap = (
debugStackEntries: DebugStackEntry[],
): Map<string, StackFrame[]> => {
const functionNameToRscFrames = new Map<string, StackFrame[]>();
for (const debugStackEntry of debugStackEntries) {
for (const stackFrame of debugStackEntry.stackFrames) {
if (!isReactServerComponentFrame(stackFrame)) continue;
const functionName = stackFrame.functionName!;
const framesForFunction = functionNameToRscFrames.get(functionName) ?? [];
const isDuplicateFrame = framesForFunction.some((existingFrame) =>
areStackFramesEqual(existingFrame, stackFrame),
);
if (!isDuplicateFrame) {
framesForFunction.push(stackFrame);
functionNameToRscFrames.set(functionName, framesForFunction);
}
}
}
return functionNameToRscFrames;
};
const getEnrichedServerStackFrame = (
serverFrame: StackFrame,
functionNameToRscFrames: Map<string, StackFrame[]>,
functionNameToUsageIndex: Map<string, number>,
): StackFrame => {
if (!serverFrame.functionName) {
return { ...serverFrame, isServer: true };
}
const availableRscFrames = functionNameToRscFrames.get(serverFrame.functionName);
if (!availableRscFrames || availableRscFrames.length === 0) {
return { ...serverFrame, isServer: true };
}
const currentUsageIndex = functionNameToUsageIndex.get(serverFrame.functionName) ?? 0;
const resolvedRscFrame = availableRscFrames[currentUsageIndex % availableRscFrames.length];
functionNameToUsageIndex.set(serverFrame.functionName, currentUsageIndex + 1);
return {
...serverFrame,
isServer: true,
fileName: resolvedRscFrame.fileName,
lineNumber: resolvedRscFrame.lineNumber,
columnNumber: resolvedRscFrame.columnNumber,
source: serverFrame.source?.replace(
SERVER_FRAME_MARKER,
`(${resolvedRscFrame.fileName}:${resolvedRscFrame.lineNumber}:${resolvedRscFrame.columnNumber})`,
),
};
};
const isServerComponentUrl = (url: string): boolean =>
SERVER_COMPONENT_URL_PREFIXES.some((prefix) => url.startsWith(prefix));
// flight installs fake server frames (rsc:// or about://React/ urls) into
// client-side debug stacks, so server detection must be per-frame
const markFlightServerFrame = (stackFrame: StackFrame): StackFrame =>
!stackFrame.isServer && stackFrame.fileName && isServerComponentUrl(stackFrame.fileName)
? { ...stackFrame, isServer: true }
: stackFrame;
/**
* Builds owner-chain stack frames from the _debugStack errors React 19
* attaches at JSX creation, walking `_debugOwner` across both client fibers
* and server components (ReactComponentInfo, which chains via `.owner` and
* carries `.debugStack`). This is exact - no re-invoking components, no
* name-matching heuristics - but requires React 19.
*/
const getOwnerStackFromDebugStacks = (fiber: Fiber): StackFrame[] => {
const ownerStackFrames: StackFrame[] = [];
let owner: Fiber | ServerComponentInfo | null | undefined = fiber;
while (owner) {
if (isFiberOwner(owner)) {
const ownerFiber: Fiber = owner;
owner = getDebugOwner(ownerFiber);
if (owner && hasDebugStack(ownerFiber)) {
const { frames, isTrusted } = parseDebugStack(ownerFiber._debugStack);
if (isTrusted) {
for (const stackFrame of frames) {
ownerStackFrames.push(markFlightServerFrame(stackFrame));
}
}
}
} else {
const serverOwner: ServerComponentInfo = owner;
owner = serverOwner.owner;
// server stacks are captured and pre-trimmed by flight, so they carry
// no bottom-frame sentinel and are trusted as-is
if (owner && serverOwner.debugStack instanceof Error) {
for (const serverFrame of parseDebugStack(serverOwner.debugStack).frames) {
ownerStackFrames.push({ ...serverFrame, isServer: true });
}
}
}
}
return ownerStackFrames;
};
const getDebugStackEntries = (rootFiber: Fiber): DebugStackEntry[] => {
const debugStackEntries: DebugStackEntry[] = [];
traverseFiber(
rootFiber,
(currentFiber) => {
if (!hasDebugStack(currentFiber)) return;
const componentName =
typeof currentFiber.type !== "string"
? getDisplayName(currentFiber.type) || "<anonymous>"
: currentFiber.type;
debugStackEntries.push({
componentName,
stackFrames: parseStack(formatOwnerStack(currentFiber._debugStack?.stack)),
});
},
true,
);
return debugStackEntries;
};
/**
* Returns a stack of ALL ancestor components in the render tree (the fiber's
* `return` chain), including wrappers that render `{children}` without having
* created this fiber's JSX. Locations come from re-invoking each component
* with a throwing dispatcher; server frames are enriched from debug stacks by
* name matching. Works on every React version.
*/
export const getParentStack = async (
fiber: Fiber,
shouldCache = true,
fetchFunction?: (url: string) => Promise<Response>,
): Promise<StackFrame[]> => {
const debugStackEntries = getDebugStackEntries(fiber);
const fallbackStackFrames = parseStack(getFallbackParentStack(fiber));
const functionNameToRscFrames = buildFunctionNameToRscFramesMap(debugStackEntries);
const functionNameToUsageIndex = new Map<string, number>();
const enrichedStackFrames = fallbackStackFrames.map((stackFrame): StackFrame => {
const isServerFrame =
(stackFrame.source?.includes(SERVER_FRAME_MARKER) ?? false) ||
(stackFrame.source != null && SERVER_ENV_PATTERN.test(stackFrame.source));
if (isServerFrame) {
return getEnrichedServerStackFrame(
stackFrame,
functionNameToRscFrames,
functionNameToUsageIndex,
);
}
return stackFrame;
});
const deduplicatedStackFrames = enrichedStackFrames.filter((stackFrame, index, frames) => {
if (index === 0) return true;
const previousFrame = frames[index - 1];
return stackFrame.functionName !== previousFrame.functionName;
});
return symbolicateStack(deduplicatedStackFrames, shouldCache, fetchFunction);
};
// an owner frame is only actionable if it can point an editor somewhere:
// it needs a file location and must not be ignore-listed bundler/framework code
const isLocatableFrame = (stackFrame: StackFrame): boolean =>
Boolean(stackFrame.fileName) && !stackFrame.isIgnoreListed;
/**
* Returns the stack of components that CREATED this fiber's JSX (the
* `_debugOwner` chain), with exact creation-site locations from React 19's
* `_debugStack` errors - including server component owners. Wrappers that
* merely render `{children}` do not appear; use {@link getParentStack} for
* the full render-tree ancestry. Falls back to {@link getParentStack} on
* React <19, when no trusted debug stacks exist, or when the owner chain
* yields no locatable frames (so callers always get the most useful stack
* available).
*/
export const getOwnerStack = async (
fiber: Fiber,
shouldCache = true,
fetchFunction?: (url: string) => Promise<Response>,
): Promise<StackFrame[]> => {
const debugStackFrames = getOwnerStackFromDebugStacks(fiber);
if (debugStackFrames.length > 0) {
// the owner chain does not include the fiber itself, but bippy's stacks
// always start with the fiber's own frame
const selfFrame: StackFrame = getDefinitionFrameFromOwnedChild(fiber) ?? {};
selfFrame.functionName = getDisplayName(fiber.type) ?? selfFrame.functionName;
const symbolicatedFrames = await symbolicateStack(
[selfFrame, ...debugStackFrames],
shouldCache,
fetchFunction,
);
const hasLocatableOwnerFrame = symbolicatedFrames.some(
(stackFrame, frameIndex) => frameIndex > 0 && isLocatableFrame(stackFrame),
);
if (hasLocatableOwnerFrame) {
return symbolicatedFrames;
}
}
return getParentStack(fiber, shouldCache, fetchFunction);
};

133
node_modules/bippy/src/source/parse-debug-stack.ts generated vendored Normal file
View File

@@ -0,0 +1,133 @@
import { JSX_FACTORY_FRAME_COUNT, REACT_STACK_BOTTOM_FRAME_PATTERNS } from "./constants.js";
import { parseStack, StackFrame } from "./parse-stack.js";
interface V8CallSite {
getFunctionName?: () => string | null;
getScriptNameOrSourceURL?: () => string | null;
getLineNumber?: () => number | null;
getColumnNumber?: () => number | null;
getEnclosingLineNumber?: () => number | null;
getEnclosingColumnNumber?: () => number | null;
getTypeName?: () => string | null;
getMethodName?: () => string | null;
getEvalOrigin?: () => string | null;
isNative?: () => boolean;
isEval?: () => boolean;
toString: () => string;
}
export interface ParsedDebugStack {
frames: StackFrame[];
// React appends a react-stack-bottom-frame sentinel to stacks captured
// during render; without it the JSX was created outside a render and the
// lower frames are arbitrary bootstrapping code
isTrusted: boolean;
}
const parsedDebugStackCache = new WeakMap<Error, ParsedDebugStack>();
const isReactBottomFrameName = (functionName: string): boolean =>
REACT_STACK_BOTTOM_FRAME_PATTERNS.some((pattern) => functionName.includes(pattern));
const getCallSiteFunctionName = (callSite: V8CallSite): string => {
const functionName = callSite.getFunctionName?.() ?? "";
if (functionName) {
return functionName;
}
const typeName = callSite.getTypeName?.() ?? "";
const methodName = callSite.getMethodName?.() ?? "";
if (typeName && methodName) {
return `${typeName}.${methodName}`;
}
return methodName;
};
const collectStructuredFrames = (callSites: V8CallSite[]): ParsedDebugStack => {
const frames: StackFrame[] = [];
for (
let callSiteIndex = JSX_FACTORY_FRAME_COUNT;
callSiteIndex < callSites.length;
callSiteIndex++
) {
const callSite = callSites[callSiteIndex];
const functionName = getCallSiteFunctionName(callSite);
if (isReactBottomFrameName(functionName)) {
return { frames, isTrusted: true };
}
if (callSite.isNative?.()) {
frames.push({ functionName: functionName || undefined });
continue;
}
let fileName = callSite.getScriptNameOrSourceURL?.() ?? "";
if (!fileName && callSite.isEval?.()) {
fileName = callSite.getEvalOrigin?.() ?? "";
}
frames.push({
functionName: functionName && functionName !== "<anonymous>" ? functionName : undefined,
fileName: fileName && fileName !== "<anonymous>" ? fileName : undefined,
lineNumber: callSite.getLineNumber?.() ?? undefined,
columnNumber: callSite.getColumnNumber?.() ?? undefined,
enclosingLineNumber: callSite.getEnclosingLineNumber?.() ?? undefined,
enclosingColumnNumber: callSite.getEnclosingColumnNumber?.() ?? undefined,
source: ` at ${callSite.toString()}`,
});
}
return { frames, isTrusted: false };
};
const parseMaterializedStack = (stackString: string): ParsedDebugStack => {
let bottomFrameIndex = -1;
for (const pattern of REACT_STACK_BOTTOM_FRAME_PATTERNS) {
bottomFrameIndex = stackString.indexOf(pattern);
if (bottomFrameIndex !== -1) break;
}
const trimmedStack =
bottomFrameIndex === -1
? stackString
: stackString.slice(0, stackString.lastIndexOf("\n", bottomFrameIndex));
return {
frames: parseStack(trimmedStack).slice(JSX_FACTORY_FRAME_COUNT),
isTrusted: bottomFrameIndex !== -1,
};
};
/**
* Parses a React `_debugStack` Error, preferring V8's structured CallSite API
* (via Error.prepareStackTrace) over string parsing. Structured frames carry
* enclosing line/column - the function definition start, not the call site -
* and are immune to source-mapped `.stack` strings. Falls back to string
* parsing on engines without CallSites (JSC, SpiderMonkey) or when the stack
* was already materialized. The leading JSX factory frame (jsxDEV) and
* everything at or below React's bottom-frame sentinel are dropped.
*/
export const parseDebugStack = (debugStack: Error): ParsedDebugStack => {
const cachedResult = parsedDebugStackCache.get(debugStack);
if (cachedResult) {
return cachedResult;
}
let structuredResult: ParsedDebugStack | null = null;
const collectFramesAndFormatStack = (error: Error, callSites: V8CallSite[]): string => {
structuredResult = collectStructuredFrames(callSites);
// this return value becomes error.stack permanently, so emit the default
// V8 format for any later reader of the same error
let stackString = `${error.name || "Error"}: ${error.message || ""}`;
for (const callSite of callSites) {
stackString += `\n at ${callSite.toString()}`;
}
return stackString;
};
const previousPrepareStackTrace = Error.prepareStackTrace;
// node's CallSite typings disagree with browser-safe optional methods
Error.prepareStackTrace = collectFramesAndFormatStack as typeof Error.prepareStackTrace;
let stackString: string;
try {
stackString = String(debugStack.stack);
} finally {
Error.prepareStackTrace = previousPrepareStackTrace;
}
const result = structuredResult ?? parseMaterializedStack(stackString);
parsedDebugStackCache.set(debugStack, result);
return result;
};

224
node_modules/bippy/src/source/parse-hook-names.ts generated vendored Normal file
View File

@@ -0,0 +1,224 @@
import type { HooksNode, HooksTree, HookSource } from "./inspect-hooks.js";
import { getSourceMap, getSourceFromSourceMap, type SourceMap } from "./symbolication.js";
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface HookNames extends Map<string, string> {}
const UNNAMED_HOOKS = new Set([
"Effect",
"LayoutEffect",
"InsertionEffect",
"ImperativeHandle",
"DebugValue",
]);
// HACK: matches `const/let/var [name, ...] = use...(...` or `const/let/var name = use...(...`
// across up to 10 lines; handles TypeScript generics like `useState<T>(`
const HOOK_DECLARATION_REGEX =
/(?:const|let|var)\s+((?:\[[\s\S]*?\]|\w+))\s*=\s*(?:[\w$.]+\.)*use[A-Z]\w*\s*(?:<[\s\S]*?>)?\s*\(/g;
export const getHookSourceLocationKey = (hookSource: HookSource): string =>
`${hookSource.fileName ?? ""}:${hookSource.lineNumber ?? 0}:${hookSource.columnNumber ?? 0}`;
const flattenHooksTree = (hooksTree: HooksTree): HooksNode[] => {
const hooksList: HooksNode[] = [];
const collectNamedHooks = (tree: HooksTree): void => {
for (const hook of tree) {
if (UNNAMED_HOOKS.has(hook.name)) continue;
hooksList.push(hook);
if (hook.subHooks.length > 0) collectNamedHooks(hook.subHooks);
}
};
collectNamedHooks(hooksTree);
return hooksList;
};
const findSourceContentByFileName = (
sources: string[],
sourcesContent: string[] | undefined,
fileName: string,
): string | null => {
if (!sourcesContent) return null;
const sourceIndex = sources.indexOf(fileName);
return sourceIndex !== -1 ? (sourcesContent[sourceIndex] ?? null) : null;
};
const getSourceContentFromSourceMap = (
sourceMap: SourceMap,
originalFileName: string,
): string | null => {
const directResult = findSourceContentByFileName(
sourceMap.sources,
sourceMap.sourcesContent,
originalFileName,
);
if (directResult) return directResult;
if (sourceMap.sections) {
for (const section of sourceMap.sections) {
const sectionResult = findSourceContentByFileName(
section.map.sources,
section.map.sourcesContent,
originalFileName,
);
if (sectionResult) return sectionResult;
}
}
return null;
};
const extractVariableNameFromBinding = (binding: string): string | null => {
const trimmed = binding.trim();
if (trimmed.startsWith("[")) {
const match = trimmed.match(/\[\s*(\w+)/);
return match ? match[1] : null;
}
return /^\w+$/.test(trimmed) ? trimmed : null;
};
export const extractHookVariableName = (
sourceCode: string,
lineNumber: number,
columnNumber: number,
): string | null => {
const lines = sourceCode.split("\n");
const hookLineIndex = lineNumber - 1;
if (hookLineIndex < 0 || hookLineIndex >= lines.length) return null;
const searchStartLine = Math.max(0, hookLineIndex - 10);
const chunkLines = lines.slice(searchStartLine, hookLineIndex + 1);
const sourceChunk = chunkLines.join("\n");
const allMatches = [...sourceChunk.matchAll(HOOK_DECLARATION_REGEX)];
const hookPositionInChunk = sourceChunk.lastIndexOf("\n") + 1 + columnNumber;
const closestMatch = allMatches.filter((match) => match.index! <= hookPositionInChunk).at(-1);
if (closestMatch) {
return extractVariableNameFromBinding(closestMatch[1]);
}
return null;
};
interface ResolvedSource {
sourceCode: string;
lineNumber: number;
columnNumber: number;
}
interface SourceResolutionContext {
sourceMapsByFile: Map<string, SourceMap | null>;
sourceContentCache: Map<string, string | null>;
fetchFn?: (url: string) => Promise<Response>;
}
const resolveOriginalSource = async (
runtimeFileName: string,
runtimeLine: number,
runtimeColumn: number,
context: SourceResolutionContext,
): Promise<ResolvedSource | null> => {
const { sourceMapsByFile, sourceContentCache, fetchFn } = context;
if (!sourceMapsByFile.has(runtimeFileName)) {
sourceMapsByFile.set(runtimeFileName, await getSourceMap(runtimeFileName, true, fetchFn));
}
const sourceMap = sourceMapsByFile.get(runtimeFileName) ?? null;
if (sourceMap) {
const originalLocation = getSourceFromSourceMap(sourceMap, runtimeLine, runtimeColumn);
if (originalLocation?.fileName && originalLocation.lineNumber !== undefined) {
const cacheKey = `sourcemap:${runtimeFileName}:${originalLocation.fileName}`;
if (!sourceContentCache.has(cacheKey)) {
sourceContentCache.set(
cacheKey,
getSourceContentFromSourceMap(sourceMap, originalLocation.fileName),
);
}
const originalSourceCode = sourceContentCache.get(cacheKey) ?? null;
if (originalSourceCode) {
return {
sourceCode: originalSourceCode,
lineNumber: originalLocation.lineNumber,
columnNumber: originalLocation.columnNumber ?? 0,
};
}
}
}
if (!sourceContentCache.has(runtimeFileName)) {
try {
const fetchImpl = fetchFn ?? fetch;
const response = await fetchImpl(runtimeFileName);
sourceContentCache.set(runtimeFileName, response.ok ? await response.text() : null);
} catch {
sourceContentCache.set(runtimeFileName, null);
}
}
const runtimeSourceCode = sourceContentCache.get(runtimeFileName) ?? null;
if (runtimeSourceCode) {
return {
sourceCode: runtimeSourceCode,
lineNumber: runtimeLine,
columnNumber: runtimeColumn,
};
}
return null;
};
export const parseHookNames = async (
hooksTree: HooksTree,
fetchFn?: (url: string) => Promise<Response>,
): Promise<HookNames> => {
const hookNames: HookNames = new Map();
const hooksList = flattenHooksTree(hooksTree);
if (hooksList.length === 0) return hookNames;
const resolutionContext: SourceResolutionContext = {
sourceMapsByFile: new Map(),
sourceContentCache: new Map(),
fetchFn,
};
await Promise.all(
hooksList.map(async (hook) => {
const hookSource = hook.hookSource;
if (
!hookSource ||
!hookSource.fileName ||
hookSource.lineNumber === null ||
hookSource.columnNumber === null
) {
return;
}
const resolved = await resolveOriginalSource(
hookSource.fileName,
hookSource.lineNumber,
hookSource.columnNumber,
resolutionContext,
);
if (!resolved) return;
const variableName = extractHookVariableName(
resolved.sourceCode,
resolved.lineNumber,
resolved.columnNumber,
);
if (variableName) {
const locationKey = getHookSourceLocationKey(hookSource);
hookNames.set(locationKey, variableName);
}
}),
);
return hookNames;
};

150
node_modules/bippy/src/source/parse-stack.ts generated vendored Normal file
View File

@@ -0,0 +1,150 @@
export interface StackFrame {
args?: unknown[];
columnNumber?: number;
lineNumber?: number;
// start of the enclosing function (the definition, not the call site);
// only available from V8's structured CallSite API
enclosingLineNumber?: number;
enclosingColumnNumber?: number;
fileName?: string;
functionName?: string;
source?: string;
isServer?: boolean;
isSymbolicated?: boolean;
// the source map ignore-listed this frame's original source (x_google_ignoreList)
isIgnoreListed?: boolean;
}
export interface ParseOptions {
slice?: number | [number, number];
allowEmpty?: boolean;
includeInElement?: boolean;
}
const FIREFOX_SAFARI_STACK_REGEXP = /(^|@)\S+:\d+/;
const CHROME_IE_STACK_REGEXP = /^\s*at .*(\S+:\d+|\(native\))/m;
const SAFARI_NATIVE_CODE_REGEXP = /^(eval@)?(\[native code\])?$/;
export const parseStack = (stackString: string, options?: ParseOptions): StackFrame[] => {
if (options?.includeInElement !== false) {
const lines = stackString.split("\n");
const frames: StackFrame[] = [];
for (const rawLine of lines) {
if (/^\s*at\s+/.test(rawLine)) {
const parsed = parseV8OrIeString(rawLine, undefined)[0];
if (parsed) frames.push(parsed);
} else if (/^\s*in\s+/.test(rawLine)) {
const elementName = rawLine.replace(/^\s*in\s+/, "").replace(/\s*\(at .*\)$/, "");
frames.push({ functionName: elementName, source: rawLine });
} else if (rawLine.match(FIREFOX_SAFARI_STACK_REGEXP)) {
const parsed = parseFFOrSafariString(rawLine, undefined)[0];
if (parsed) frames.push(parsed);
}
}
return applySlice(frames, options);
}
if (stackString.match(CHROME_IE_STACK_REGEXP)) {
return parseV8OrIeString(stackString, options);
}
return parseFFOrSafariString(stackString, options);
};
export const extractLocation = (
urlLike: string,
): [string, string | undefined, string | undefined] => {
if (!urlLike.includes(":")) return [urlLike, undefined, undefined];
// HACK: Chrome/V8 stack traces wrap location in parens: "(file.js:10:5)"
// We need to strip these outer parens but preserve parens in paths (e.g., Next.js route groups like "(docs)")
// Chrome format always ends with `:col)` where digit comes right before the closing paren
const isWrappedLocation = urlLike.startsWith("(") && /:\d+\)$/.test(urlLike);
const sanitizedResult = isWrappedLocation ? urlLike.slice(1, -1) : urlLike;
const regExp = /(.+?)(?::(\d+))?(?::(\d+))?$/;
const parts = regExp.exec(sanitizedResult);
if (!parts) return [sanitizedResult, undefined, undefined];
return [parts[1], parts[2] || undefined, parts[3] || undefined] as const;
};
const applySlice = <T>(lines: T[], options?: ParseOptions): T[] => {
if (options && options.slice != null) {
if (Array.isArray(options.slice)) return lines.slice(options.slice[0], options.slice[1]);
return lines.slice(0, options.slice);
}
return lines;
};
export const parseV8OrIeString = (stack: string, options?: ParseOptions): StackFrame[] => {
const filteredLines = applySlice(
stack.split("\n").filter((line) => {
return !!line.match(CHROME_IE_STACK_REGEXP);
}),
options,
);
return filteredLines.map((line): StackFrame => {
let currentLine = line;
if (currentLine.includes("(eval ")) {
currentLine = currentLine
.replace(/eval code/g, "eval")
.replace(/(\(eval at [^()]*)|(,.*$)/g, "");
}
let sanitizedLine = currentLine
.replace(/^\s+/, "")
.replace(/\(eval code/g, "(")
.replace(/^.*?\s+/, "");
const locationMatch = sanitizedLine.match(/ (\(.+\)$)/);
sanitizedLine = locationMatch ? sanitizedLine.replace(locationMatch[0], "") : sanitizedLine;
const locationParts = extractLocation(locationMatch ? locationMatch[1] : sanitizedLine);
const functionName = (locationMatch && sanitizedLine) || undefined;
const fileName = ["eval", "<anonymous>"].includes(locationParts[0])
? undefined
: locationParts[0];
return {
functionName,
fileName,
lineNumber: locationParts[1] ? +locationParts[1] : undefined,
columnNumber: locationParts[2] ? +locationParts[2] : undefined,
source: currentLine,
};
});
};
export const parseFFOrSafariString = (stack: string, options?: ParseOptions): StackFrame[] => {
const filteredLines = applySlice(
stack.split("\n").filter((line) => {
return !line.match(SAFARI_NATIVE_CODE_REGEXP);
}),
options,
);
return filteredLines.map((line): StackFrame => {
let currentLine = line;
if (currentLine.includes(" > eval"))
currentLine = currentLine.replace(/ line (\d+)(?: > eval line \d+)* > eval:\d+:\d+/g, ":$1");
if (!currentLine.includes("@") && !currentLine.includes(":")) {
return {
functionName: currentLine,
};
} else {
const functionNameRegex =
/(([^\n\r"\u2028\u2029]*".[^\n\r"\u2028\u2029]*"[^\n\r@\u2028\u2029]*(?:@[^\n\r"\u2028\u2029]*"[^\n\r@\u2028\u2029]*)*(?:[\n\r\u2028\u2029][^@]*)?)?[^@]*)@/;
const matches = currentLine.match(functionNameRegex);
const functionName = matches && matches[1] ? matches[1] : undefined;
const locationParts = extractLocation(currentLine.replace(functionNameRegex, ""));
return {
functionName,
fileName: locationParts[0],
lineNumber: locationParts[1] ? +locationParts[1] : undefined,
columnNumber: locationParts[2] ? +locationParts[2] : undefined,
source: currentLine,
};
}
});
};

398
node_modules/bippy/src/source/symbolication.ts generated vendored Normal file
View File

@@ -0,0 +1,398 @@
import { decode, SourceMapMappings, type SourceMapSegment } from "@jridgewell/sourcemap-codec";
import { StackFrame } from "./parse-stack.js";
export interface DecodedSourceMapSection {
map: {
file?: string;
ignoredSourceIndices?: Set<number>;
mappings: SourceMapSegment[][];
names?: string[];
sourceRoot?: string;
sources: string[];
sourcesContent?: string[];
version: 3;
};
offset: {
column: number;
line: number;
};
}
// https://tc39.es/ecma426/#sec-index-source-map
export interface IndexSourceMap {
file?: string;
sections: Array<{
map: StandardSourceMap;
offset: {
column: number;
line: number;
};
}>;
version: 3;
}
export type RawSourceMap = IndexSourceMap | StandardSourceMap;
export interface SourceMap {
file?: string;
ignoredSourceIndices?: Set<number>;
mappings: SourceMapSegment[][];
names?: string[];
sections?: DecodedSourceMapSection[];
sourceRoot?: string;
sources: string[];
sourcesContent?: string[];
version: 3;
}
// https://developer.chrome.com/blog/sourcemaps#the_anatomy_of_a_source_map
export interface StandardSourceMap {
file?: string;
ignoreList?: number[];
mappings: string;
names?: string[];
sourceRoot?: string;
sources: string[];
sourcesContent?: string[];
version: 3;
x_google_ignoreList?: number[];
}
// has a scheme, e.g. http://, https://, file://, data:, etc.
// https://datatracker.ietf.org/doc/html/rfc3986#section-3.1
const SCHEME_REGEX = /^[a-zA-Z][a-zA-Z\d+\-.]*:/;
// inline sourcemap, e.g. data:application/json;base64,...
const INLINE_SOURCEMAP_REGEX = /^data:application\/json[^,]+base64,/;
// sourcemap url, e.g. //@ sourceMappingURL=... or /* @ sourceMappingURL=... */ at the end of the file
const SOURCEMAP_REGEX =
/(?:\/\/[@#][ \t]+sourceMappingURL=([^\s'"]+?)[ \t]*$)|(?:\/\*[@#][ \t]+sourceMappingURL=([^*]+?)[ \t]*(?:\*\/)[ \t]*$)/;
export const sourceMapCache = new Map<string, null | SourceMap>();
interface SourceMapResult {
sourceMap: null | SourceMap;
isTransientFailure: boolean;
}
const _pendingSourceMapRequests = new Map<string, Promise<SourceMapResult>>();
const getSourceFromMappings = (
mappings: SourceMapMappings,
sources: string[],
lineIndexInMappings: number,
column: number,
ignoredSourceIndices?: Set<number>,
): StackFrame | null => {
if (lineIndexInMappings < 0 || lineIndexInMappings >= mappings.length) {
return null;
}
const lineMapping = mappings[lineIndexInMappings];
if (!lineMapping || lineMapping.length === 0) {
return null;
}
// Segments within a line are sorted by generated column, so binary search for
// the last segment at or before the column.
let closestLineSegment: null | SourceMapSegment = null;
let lowIndex = 0;
let highIndex = lineMapping.length - 1;
while (lowIndex <= highIndex) {
const middleIndex = (lowIndex + highIndex) >> 1;
if (lineMapping[middleIndex][0] <= column) {
closestLineSegment = lineMapping[middleIndex];
lowIndex = middleIndex + 1;
} else {
highIndex = middleIndex - 1;
}
}
if (!closestLineSegment || closestLineSegment.length < 4) {
return null;
}
const [, sourceIndex, sourceLine, sourceColumn] = closestLineSegment;
if (sourceIndex === undefined || sourceLine === undefined || sourceColumn === undefined) {
return null;
}
const fileName = sources[sourceIndex];
if (!fileName) {
return null;
}
return {
columnNumber: sourceColumn,
fileName,
lineNumber: sourceLine + 1,
isIgnoreListed: ignoredSourceIndices?.has(sourceIndex) ?? false,
};
};
export const getSourceFromSourceMap = (
sourceMap: SourceMap,
line: number,
column: number,
): StackFrame | null => {
if (sourceMap.sections) {
// Section offsets are 0-based while stack trace lines are 1-based.
const lineIndex = line - 1;
let targetSection: DecodedSourceMapSection | null = null;
for (const section of sourceMap.sections) {
if (
lineIndex > section.offset.line ||
(lineIndex === section.offset.line && column >= section.offset.column)
) {
targetSection = section;
} else {
break;
}
}
if (!targetSection) {
return null;
}
const relativeLine = lineIndex - targetSection.offset.line;
const relativeColumn =
lineIndex === targetSection.offset.line ? column - targetSection.offset.column : column;
return getSourceFromMappings(
targetSection.map.mappings,
targetSection.map.sources,
relativeLine,
relativeColumn,
targetSection.map.ignoredSourceIndices,
);
}
return getSourceFromMappings(
sourceMap.mappings,
sourceMap.sources,
line - 1,
column,
sourceMap.ignoredSourceIndices,
);
};
const getSourceMapUrl = (url: string, content: string): null | string => {
// Walk lines backwards without content.split("\n"), which would allocate a
// string per line of the entire bundle.
let sourceMapUrl: string | undefined;
let searchEnd = content.length;
while (searchEnd > 0 && !sourceMapUrl) {
const lineStart = content.lastIndexOf("\n", searchEnd - 1) + 1;
const regexMatch = content.slice(lineStart, searchEnd).match(SOURCEMAP_REGEX);
if (regexMatch) {
sourceMapUrl = regexMatch[1] || regexMatch[2];
}
searchEnd = lineStart - 1;
}
if (!sourceMapUrl) {
return null;
}
const hasScheme = SCHEME_REGEX.test(sourceMapUrl);
if (!(INLINE_SOURCEMAP_REGEX.test(sourceMapUrl) || hasScheme || sourceMapUrl.startsWith("/"))) {
const urlSegments = url.split("/");
urlSegments[urlSegments.length - 1] = sourceMapUrl;
sourceMapUrl = urlSegments.join("/");
}
return sourceMapUrl;
};
const getIgnoredSourceIndices = (rawSourceMap: StandardSourceMap): Set<number> | undefined => {
const ignoreList = rawSourceMap.ignoreList ?? rawSourceMap.x_google_ignoreList;
return Array.isArray(ignoreList) && ignoreList.length > 0 ? new Set(ignoreList) : undefined;
};
const decodeStandardSourceMap = (rawSourceMap: StandardSourceMap): SourceMap => ({
file: rawSourceMap.file,
ignoredSourceIndices: getIgnoredSourceIndices(rawSourceMap),
mappings: decode(rawSourceMap.mappings),
names: rawSourceMap.names,
sourceRoot: rawSourceMap.sourceRoot,
sources: rawSourceMap.sources,
sourcesContent: rawSourceMap.sourcesContent,
version: 3,
});
const decodeIndexSourceMap = (rawSourceMap: IndexSourceMap): SourceMap => {
const decodedSections: DecodedSourceMapSection[] = rawSourceMap.sections.map(
({ map, offset }) => ({
map: {
...map,
ignoredSourceIndices: getIgnoredSourceIndices(map),
mappings: decode(map.mappings),
},
offset,
}),
);
const allSources = new Set<string>();
for (const section of decodedSections) {
for (const source of section.map.sources) {
allSources.add(source);
}
}
return {
file: rawSourceMap.file,
mappings: [],
names: [],
sections: decodedSections,
sourceRoot: undefined,
sources: Array.from(allSources),
sourcesContent: undefined,
version: 3,
};
};
const isFetchableUrl = (url: string): boolean => {
if (!url) {
return false;
}
const trimmedUrl = url.trim();
if (!trimmedUrl) {
return false;
}
const schemeMatch = trimmedUrl.match(SCHEME_REGEX);
if (!schemeMatch) {
return true;
}
const scheme = schemeMatch[0].toLowerCase();
return scheme === "http:" || scheme === "https:";
};
// Resolves a bundle's source map, or null when the bundle definitively has
// none. A thrown fetch (network error or aborted request) is left to propagate
// so getSourceMap can treat it as transient and avoid caching it: a non-ok
// response, a missing sourceMappingURL, or an undecodable map are definitive and
// return null, but a dropped connection is not and must stay retryable.
export const getSourceMapImpl = async (
bundleUrl: string,
fetchFn: (url: string) => Promise<Response> = fetch,
): Promise<null | SourceMap> => {
if (!isFetchableUrl(bundleUrl)) {
return null;
}
const bundleResponse = await fetchFn(bundleUrl);
if (!bundleResponse.ok) {
return null;
}
const bundleContent = await bundleResponse.text();
if (!bundleContent) {
return null;
}
const sourceMapUrl = getSourceMapUrl(bundleUrl, bundleContent);
if (!sourceMapUrl) return null;
// inline data: maps (vite dev, babel inline sourcemaps) are decoded by
// fetch itself, so they bypass the network-url check
if (!isFetchableUrl(sourceMapUrl) && !INLINE_SOURCEMAP_REGEX.test(sourceMapUrl)) {
return null;
}
const sourceMapResponse = await fetchFn(sourceMapUrl);
if (!sourceMapResponse.ok) {
return null;
}
try {
const rawSourceMap = (await sourceMapResponse.json()) as RawSourceMap;
return "sections" in rawSourceMap
? decodeIndexSourceMap(rawSourceMap)
: decodeStandardSourceMap(rawSourceMap);
} catch {
return null;
}
};
export const getSourceMap = async (
file: string,
useCache = true,
fetchFn?: (url: string) => Promise<Response>,
): Promise<null | SourceMap> => {
if (useCache && sourceMapCache.has(file)) {
return sourceMapCache.get(file) ?? null;
}
const pendingRequest = useCache ? _pendingSourceMapRequests.get(file) : undefined;
if (pendingRequest) {
return (await pendingRequest).sourceMap;
}
// A transient fetch failure (aborted request or network error) rejects; a
// definitive "no map" resolves to null. Only definitive results are cached:
// caching a transient null would pin the bundle to a degraded result for the
// rest of the page's lifetime, even after the network recovers.
const fetchPromise: Promise<SourceMapResult> = getSourceMapImpl(file, fetchFn).then(
(sourceMap) => ({ sourceMap, isTransientFailure: false }),
() => ({ sourceMap: null, isTransientFailure: true }),
);
if (useCache) {
_pendingSourceMapRequests.set(file, fetchPromise);
}
const { sourceMap, isTransientFailure } = await fetchPromise;
if (useCache) {
_pendingSourceMapRequests.delete(file);
if (!isTransientFailure) {
sourceMapCache.set(file, sourceMap);
}
}
return sourceMap;
};
export const symbolicateStack = async (
stack: StackFrame[],
cache = true,
fetchFn?: (url: string) => Promise<Response>,
): Promise<StackFrame[]> => {
return await Promise.all(
stack.map(async (stackFrame) => {
if (!stackFrame.fileName) return stackFrame;
const sourceMap = await getSourceMap(stackFrame.fileName, cache, fetchFn);
if (
!sourceMap ||
typeof stackFrame.lineNumber !== "number" ||
typeof stackFrame.columnNumber !== "number"
) {
return stackFrame;
}
const symbolicatedSource = getSourceFromSourceMap(
sourceMap,
stackFrame.lineNumber,
stackFrame.columnNumber,
);
if (!symbolicatedSource) return stackFrame;
return {
...stackFrame,
source:
symbolicatedSource.fileName && stackFrame.source
? stackFrame.source.replace(stackFrame.fileName, symbolicatedSource.fileName)
: stackFrame.source,
fileName: symbolicatedSource.fileName,
lineNumber: symbolicatedSource.lineNumber,
columnNumber: symbolicatedSource.columnNumber,
isIgnoreListed: symbolicatedSource.isIgnoreListed,
isSymbolicated: true,
};
}),
);
};

6
node_modules/bippy/src/source/types.ts generated vendored Normal file
View File

@@ -0,0 +1,6 @@
export interface FiberSource {
columnNumber?: number;
fileName: string;
lineNumber?: number;
functionName?: string;
}

438
node_modules/bippy/src/types.ts generated vendored Normal file
View File

@@ -0,0 +1,438 @@
import type { ReactNode } from "react";
// @types/react-reconciler uses `export = ReactReconciler` (CJS namespace),
// which downstream bundlers can't resolve as ESM exports. These types are
// inlined from @types/react-reconciler@0.28 to avoid the dependency.
// Simple type aliases — most are opaque in the original
export type BundleType = 0 | 1;
export type Flags = number;
export type Lanes = number;
export type TypeOfMode = number;
export type RootTag = 0 | 1 | 2;
export type LanePriority =
| 0
| 1
| 2
| 3
| 4
| 5
| 6
| 7
| 8
| 9
| 10
| 11
| 12
| 13
| 14
| 15
| 16
| 17;
export type WorkTag =
| 0
| 1
| 2
| 3
| 4
| 5
| 6
| 7
| 8
| 9
| 10
| 11
| 12
| 13
| 14
| 15
| 16
| 17
| 18
| 19
| 20
| 21
| 22
| 23
| 24
| 25
| 26
| 27
| 28
| 29
| 30
| 31;
export type HookType =
| "useState"
| "useReducer"
| "useContext"
| "useRef"
| "useEffect"
| "useLayoutEffect"
| "useCallback"
| "useMemo"
| "useImperativeHandle"
| "useDebugValue"
| "useDeferredValue"
| "useTransition"
| "useMutableSource"
| "useOpaqueIdentifier"
| "useCacheRefresh";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type FiberRoot = any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type MutableSource = any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type OpaqueHandle = any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type OpaqueRoot = any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type React$AbstractComponent<_Config, _Instance = unknown> = any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type HostConfig = Record<string, any>;
// Structural interfaces
export interface Source {
fileName: string;
lineNumber: number;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export interface RefObject {
current: any;
}
export interface Thenable<T> {
then(resolve: () => T, reject?: () => T): T;
}
export interface ReactContext<T> {
$$typeof: symbol | number;
Consumer: ReactContext<T>;
Provider: ReactProviderType<T>;
_calculateChangedBits: ((a: T, b: T) => number) | null;
_currentValue: T;
_currentValue2: T;
_threadCount: number;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
_currentRenderer?: Record<string, any> | null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
_currentRenderer2?: Record<string, any> | null;
displayName?: string;
}
export interface ReactProviderType<T> {
$$typeof: symbol | number;
_context: ReactContext<T>;
}
export interface ReactProvider<T> {
$$typeof: symbol | number;
type: ReactProviderType<T>;
key: null | string;
ref: null;
props: { value: T; children?: ReactNode };
}
export interface ReactConsumer<T> {
$$typeof: symbol | number;
type: ReactContext<T>;
key: null | string;
ref: null;
props: { children: (value: T) => ReactNode; unstable_observedBits?: number };
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export interface ReactPortal {
$$typeof: symbol | number;
key: null | string;
containerInfo: any;
children: ReactNode;
implementation: any;
}
export interface ComponentSelector {
$$typeof: symbol | number;
value: React$AbstractComponent<never, unknown>;
}
export interface HasPseudoClassSelector {
$$typeof: symbol | number;
value: Selector[];
}
export interface RoleSelector {
$$typeof: symbol | number;
value: string;
}
export interface TextSelector {
$$typeof: symbol | number;
value: string;
}
export interface TestNameSelector {
$$typeof: symbol | number;
value: string;
}
export type Selector =
| ComponentSelector
| HasPseudoClassSelector
| RoleSelector
| TextSelector
| TestNameSelector;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export interface DevToolsConfig<
Instance = any,
TextInstance = any,
RendererInspectionConfig = any,
> {
bundleType: BundleType;
version: string;
rendererPackageName: string;
findFiberByHostInstance?: (instance: Instance | TextInstance) => ReactFiber | null;
rendererConfig?: RendererInspectionConfig;
}
export interface SuspenseHydrationCallbacks<SuspenseInstance = unknown> {
onHydrated?: (suspenseInstance: SuspenseInstance) => void;
onDeleted?: (suspenseInstance: SuspenseInstance) => void;
}
export interface TransitionTracingCallbacks {
onTransitionStart?: (transitionName: string, startTime: number) => void;
onTransitionProgress?: (
transitionName: string,
startTime: number,
currentTime: number,
pending: Array<{ name: null | string }>,
) => void;
onTransitionIncomplete?: (
transitionName: string,
startTime: number,
deletions: Array<{ type: string; name?: string; newName?: string; endTime: number }>,
) => void;
onTransitionComplete?: (transitionName: string, startTime: number, endTime: number) => void;
onMarkerProgress?: (
transitionName: string,
marker: string,
startTime: number,
currentTime: number,
pending: Array<{ name: null | string }>,
) => void;
onMarkerIncomplete?: (
transitionName: string,
marker: string,
startTime: number,
deletions: Array<{ type: string; name?: string; newName?: string; endTime: number }>,
) => void;
onMarkerComplete?: (
transitionName: string,
marker: string,
startTime: number,
endTime: number,
) => void;
}
// The base Fiber interface from react-reconciler, used to derive bippy's Fiber below
interface ReactFiber {
tag: WorkTag;
key: null | string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
elementType: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
stateNode: any;
return: ReactFiber | null;
child: ReactFiber | null;
sibling: ReactFiber | null;
index: number;
ref: null | (((handle: unknown) => void) & { _stringRef?: string | null }) | RefObject;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
pendingProps: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
memoizedProps: any;
updateQueue: unknown;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
memoizedState: any;
dependencies: Dependencies | null;
mode: TypeOfMode;
flags: Flags;
subtreeFlags: Flags;
deletions: ReactFiber[] | null;
nextEffect: ReactFiber | null;
firstEffect: ReactFiber | null;
lastEffect: ReactFiber | null;
lanes: Lanes;
childLanes: Lanes;
alternate: ReactFiber | null;
actualDuration?: number;
actualStartTime?: number;
selfBaseDuration?: number;
treeBaseDuration?: number;
_debugID?: number;
_debugSource?: Source | null;
_debugOwner?: ReactFiber | null;
_debugIsCurrentlyTiming?: boolean;
_debugNeedsRemount?: boolean;
_debugHookTypes?: HookType[] | null;
}
// ── bippy types (not from react-reconciler) ──
export interface ContextDependency<T> {
context: ReactContext<T>;
memoizedValue: T;
next: ContextDependency<unknown> | null;
observedBits: number;
}
export interface Dependencies {
firstContext: ContextDependency<unknown> | null;
lanes: Lanes;
}
export interface Effect {
[key: string]: unknown;
create: (...args: unknown[]) => unknown;
deps: null | unknown[];
destroy: ((...args: unknown[]) => unknown) | null;
next: Effect | null;
tag: number;
}
export interface Family {
current: unknown;
}
/**
* React 19 flight metadata for a server component owner (ReactComponentInfo).
* Unlike client owners it has no `tag`; the owner chain continues via `owner`.
*/
export interface ServerComponentInfo {
name?: string;
env?: string;
owner?: Fiber | ServerComponentInfo | null;
debugStack?: Error | null;
}
export interface RendererRefreshUpdate {
staleFamilies: Set<Family>;
updatedFamilies: Set<Family>;
}
/**
* Represents a react-internal Fiber node.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type Fiber<T = any> = Omit<
ReactFiber,
| "alternate"
| "child"
| "dependencies"
| "memoizedProps"
| "memoizedState"
| "pendingProps"
| "return"
| "sibling"
| "stateNode"
| "updateQueue"
> & {
_debugInfo?: Array<{
debugLocation?: unknown;
env?: string;
name?: string;
}>;
_debugOwner?: Fiber;
// react <19
_debugSource?: {
columnNumber?: number;
fileName: string;
lineNumber: number;
};
// react 19+
// https://github.com/facebook/react/issues/29092?utm_source=chatgpt.com
_debugStack?: Error & { stack: string };
alternate: Fiber | null;
child: Fiber | null;
dependencies: Dependencies | null;
memoizedProps: Props;
memoizedState: MemoizedState;
pendingProps: Props;
return: Fiber | null;
sibling: Fiber | null;
stateNode: T;
updateQueue: {
[key: string]: unknown;
lastEffect: Effect | null;
};
};
export interface MemoizedState {
[key: string]: unknown;
memoizedState: unknown;
next: MemoizedState | null;
}
export interface Props {
[key: string]: unknown;
}
export interface ReactDevToolsGlobalHook {
_instrumentationIsActive?: boolean;
_instrumentationSource?: string;
checkDCE: (fn: unknown) => void;
hasUnsupportedRendererAttached: boolean;
inject: (renderer: ReactRenderer) => number;
// https://github.com/aidenybai/bippy/issues/43
on: () => void;
onCommitFiberRoot: (rendererID: number, root: FiberRoot, priority: number | void) => void;
onCommitFiberUnmount: (rendererID: number, fiber: Fiber) => void;
onPostCommitFiberRoot: (rendererID: number, root: FiberRoot) => void;
// called by dev builds of react-reconciler on root schedule; absent from
// the hook react-refresh installs, so it stays optional
onScheduleFiberRoot?: (rendererID: number, root: FiberRoot, children: ReactNode) => void;
renderers: Map<number, ReactRenderer>;
supportsFiber: boolean;
supportsFlight: boolean;
}
// https://github.com/facebook/react/blob/6a4b46cd70d2672bc4be59dcb5b8dede22ed0cef/packages/react-devtools-shared/src/backend/types.js
export interface ReactRenderer {
bundleType: 0 /* PROD */ | 1 /* DEV */;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
currentDispatcherRef: any;
// dev only: https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberReconciler.js#L842
findFiberByHostInstance?: (hostInstance: unknown) => Fiber | null;
// react devtools
getCurrentFiber?: (fiber: Fiber) => Fiber | null;
overrideContext?: (fiber: Fiber, contextType: unknown, path: string[], value: unknown) => void;
overrideHookState?: (fiber: Fiber, id: string, path: string[], value: unknown) => void;
overrideHookStateDeletePath?: (fiber: Fiber, id: number, path: Array<number | string>) => void;
overrideHookStateRenamePath?: (
fiber: Fiber,
id: number,
oldPath: Array<number | string>,
newPath: Array<number | string>,
) => void;
overrideProps?: (fiber: Fiber, path: string[], value: unknown) => void;
overridePropsDeletePath?: (fiber: Fiber, path: Array<number | string>) => void;
overridePropsRenamePath?: (
fiber: Fiber,
oldPath: Array<number | string>,
newPath: Array<number | string>,
) => void;
reconcilerVersion: string;
rendererPackageName: string;
// react refresh
scheduleRefresh?: (root: FiberRoot, update: RendererRefreshUpdate) => void;
scheduleRoot?: (root: FiberRoot, element: React.ReactNode) => void;
scheduleUpdate?: (fiber: Fiber) => void;
setErrorHandler?: (newShouldErrorImpl: (fiber: Fiber) => boolean) => void;
setRefreshHandler?: (handler: ((fiber: Fiber) => Family | null) | null) => void;
setSuspenseHandler?: (newShouldSuspendImpl: (suspenseInstance: unknown) => void) => void;
version: string;
}
declare global {
// eslint-disable-next-line no-var
var __REACT_DEVTOOLS_GLOBAL_HOOK__: ReactDevToolsGlobalHook | undefined;
}

17
node_modules/bippy/src/unsubscribe.ts generated vendored Normal file
View File

@@ -0,0 +1,17 @@
export interface Unsubscribe extends Disposable {
(): void;
}
/**
* Wraps a teardown callback so it is both callable and a `Disposable`,
* letting subscriptions compose through explicit resource management:
*
* @example
* ```ts
* using instrumentation = instrument({ onCommitFiberRoot });
* using refresh = instrumentReactRefresh({ onRefresh: handleRefresh });
* // both torn down automatically at scope exit
* ```
*/
export const toUnsubscribe = (dispose: () => void): Unsubscribe =>
Object.assign(dispose, { [Symbol.dispose]: dispose });