Add getReactSource to extract React component source location
- Add bippy library for React fiber introspection - Create build-bippy.ts and build-selector-generator.ts using Bun.build - Use CDP Runtime.evaluate instead of addScriptTag to bypass CSP - Add getReactSource utility function exposed in MCP VMContext - Works on local React dev servers with JSX transform (not production builds)
This commit is contained in:
@@ -1,5 +1,18 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.0.28
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- **Added `getReactSource` utility**: Extract React component source location (file, line, column) from DOM elements
|
||||||
|
- Uses bippy library for React fiber introspection
|
||||||
|
- Returns `{ fileName, lineNumber, columnNumber, componentName }` or `null`
|
||||||
|
- Only works on local dev servers (Vite, Next.js, CRA) with JSX transform in development mode
|
||||||
|
- **CSP bypass for script injection**: Changed `getLocatorStringForElement` and `getReactSource` to use CDP `Runtime.evaluate` instead of `addScriptTag`
|
||||||
|
- Scripts now work on pages with strict Content Security Policy
|
||||||
|
- **Switched to Bun.build**: Replaced esbuild and esm.sh downloads with Bun.build for bundling selector-generator and bippy
|
||||||
|
- New `build-selector-generator.ts` and `build-bippy.ts` scripts
|
||||||
|
|
||||||
## 0.0.27
|
## 0.0.27
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "playwriter",
|
"name": "playwriter",
|
||||||
"description": "",
|
"description": "",
|
||||||
"version": "0.0.27",
|
"version": "0.0.28",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"repository": "https://github.com/remorses/playwriter",
|
"repository": "https://github.com/remorses/playwriter",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "rm -rf dist *.tsbuildinfo && mkdir dist && cp src/resource.md src/prompt.md src/debugger-examples.ts dist/ && vite-node scripts/download-selector-generator.ts && tsc",
|
"build": "rm -rf dist *.tsbuildinfo && mkdir dist && cp src/resource.md src/prompt.md src/debugger-examples.ts dist/ && bun scripts/build-selector-generator.ts && bun scripts/build-bippy.ts && tsc",
|
||||||
"prepublishOnly": "pnpm build",
|
"prepublishOnly": "pnpm build",
|
||||||
"watch": "tsc -w",
|
"watch": "tsc -w",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
@@ -32,6 +32,7 @@
|
|||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"@vitest/ui": "^4.0.8",
|
"@vitest/ui": "^4.0.8",
|
||||||
|
"bippy": "^0.5.27",
|
||||||
"image-size": "^2.0.2",
|
"image-size": "^2.0.2",
|
||||||
"mcp-extension": "workspace:*",
|
"mcp-extension": "workspace:*",
|
||||||
"vite-node": "^5.0.0",
|
"vite-node": "^5.0.0",
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import fs from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
const distDir = path.join(__dirname, '..', 'dist')
|
||||||
|
|
||||||
|
const ENTRY_CODE = `
|
||||||
|
import { getFiberFromHostInstance, getDisplayName, traverseFiber, isCompositeFiber, isHostFiber } from 'bippy'
|
||||||
|
import { getSource, getOwnerStack, normalizeFileName, isSourceFile } from 'bippy/source'
|
||||||
|
|
||||||
|
globalThis.__bippy = {
|
||||||
|
getFiberFromHostInstance,
|
||||||
|
getDisplayName,
|
||||||
|
traverseFiber,
|
||||||
|
isCompositeFiber,
|
||||||
|
isHostFiber,
|
||||||
|
getSource,
|
||||||
|
getOwnerStack,
|
||||||
|
normalizeFileName,
|
||||||
|
isSourceFile,
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
if (!fs.existsSync(distDir)) {
|
||||||
|
fs.mkdirSync(distDir, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
const entryPath = path.join(distDir, '_bippy-entry.js')
|
||||||
|
fs.writeFileSync(entryPath, ENTRY_CODE)
|
||||||
|
|
||||||
|
console.log('Bundling bippy...')
|
||||||
|
|
||||||
|
const result = await Bun.build({
|
||||||
|
entrypoints: [entryPath],
|
||||||
|
target: 'browser',
|
||||||
|
format: 'iife',
|
||||||
|
define: {
|
||||||
|
'process.env.NODE_ENV': '"development"',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
fs.unlinkSync(entryPath)
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
console.error('Bundle errors:', result.logs)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const bundledCode = await result.outputs[0].text()
|
||||||
|
const outputPath = path.join(distDir, 'bippy.js')
|
||||||
|
fs.writeFileSync(outputPath, bundledCode)
|
||||||
|
console.log(`Saved to ${outputPath} (${Math.round(bundledCode.length / 1024)}kb)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import fs from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
const distDir = path.join(__dirname, '..', 'dist')
|
||||||
|
|
||||||
|
const ENTRY_CODE = `
|
||||||
|
import { createSelectorGenerator, toLocator } from '@mizchi/selector-generator'
|
||||||
|
|
||||||
|
globalThis.__selectorGenerator = { createSelectorGenerator, toLocator }
|
||||||
|
`
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
if (!fs.existsSync(distDir)) {
|
||||||
|
fs.mkdirSync(distDir, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
const entryPath = path.join(distDir, '_selector-generator-entry.js')
|
||||||
|
fs.writeFileSync(entryPath, ENTRY_CODE)
|
||||||
|
|
||||||
|
console.log('Bundling selector-generator...')
|
||||||
|
|
||||||
|
const result = await Bun.build({
|
||||||
|
entrypoints: [entryPath],
|
||||||
|
target: 'browser',
|
||||||
|
format: 'iife',
|
||||||
|
})
|
||||||
|
|
||||||
|
fs.unlinkSync(entryPath)
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
console.error('Bundle errors:', result.logs)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const bundledCode = await result.outputs[0].text()
|
||||||
|
const outputPath = path.join(distDir, 'selector-generator.js')
|
||||||
|
fs.writeFileSync(outputPath, bundledCode)
|
||||||
|
console.log(`Saved to ${outputPath} (${Math.round(bundledCode.length / 1024)}kb)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import fs from 'node:fs'
|
|
||||||
import path from 'node:path'
|
|
||||||
import { fileURLToPath } from 'node:url'
|
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
||||||
const distDir = path.join(__dirname, '..', 'dist')
|
|
||||||
|
|
||||||
const SELECTOR_GENERATOR_URL = 'https://esm.sh/@mizchi/selector-generator@1.50.0-next/es2022/selector-generator.bundle.mjs'
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
if (!fs.existsSync(distDir)) {
|
|
||||||
fs.mkdirSync(distDir, { recursive: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Downloading selector-generator from esm.sh...')
|
|
||||||
const response = await fetch(SELECTOR_GENERATOR_URL)
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to download: ${response.status} ${response.statusText}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
let esmScript = await response.text()
|
|
||||||
|
|
||||||
const exportMatch = esmScript.match(/export\s*\{([^}]+)\}\s*;?\s*(\/\/.*)?$/)
|
|
||||||
if (!exportMatch) {
|
|
||||||
throw new Error('Could not find export statement in bundle')
|
|
||||||
}
|
|
||||||
|
|
||||||
const exports = exportMatch[1].split(',').map((e) => {
|
|
||||||
const parts = e.trim().split(/\s+as\s+/)
|
|
||||||
return { local: parts[0].trim(), exported: (parts[1] || parts[0]).trim() }
|
|
||||||
})
|
|
||||||
|
|
||||||
const createSelectorGenerator = exports.find((e) => e.exported === 'createSelectorGenerator')
|
|
||||||
const toLocator = exports.find((e) => e.exported === 'toLocator')
|
|
||||||
|
|
||||||
if (!createSelectorGenerator || !toLocator) {
|
|
||||||
throw new Error('Could not find createSelectorGenerator or toLocator exports')
|
|
||||||
}
|
|
||||||
|
|
||||||
esmScript = esmScript.replace(/export\s*\{[^}]+\}\s*;?\s*(\/\/.*)?$/, '')
|
|
||||||
|
|
||||||
const wrappedScript = `(function() {
|
|
||||||
${esmScript}
|
|
||||||
globalThis.__selectorGenerator = { createSelectorGenerator: ${createSelectorGenerator.local}, toLocator: ${toLocator.local} };
|
|
||||||
})();`
|
|
||||||
|
|
||||||
const outputPath = path.join(distDir, 'selector-generator.js')
|
|
||||||
fs.writeFileSync(outputPath, wrappedScript)
|
|
||||||
console.log(`Saved to ${outputPath}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((err) => {
|
|
||||||
console.error(err)
|
|
||||||
process.exit(1)
|
|
||||||
})
|
|
||||||
@@ -2809,4 +2809,84 @@ describe('CDP Session Tests', () => {
|
|||||||
await browser.close()
|
await browser.close()
|
||||||
await page.close()
|
await page.close()
|
||||||
}, 60000)
|
}, 60000)
|
||||||
|
|
||||||
|
it('should inject bippy and find React fiber with getReactSource', async () => {
|
||||||
|
const browserContext = getBrowserContext()
|
||||||
|
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
||||||
|
|
||||||
|
const page = await browserContext.newPage()
|
||||||
|
await page.setContent(`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
|
||||||
|
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script>
|
||||||
|
function MyComponent() {
|
||||||
|
return React.createElement('button', { id: 'react-btn' }, 'Click me');
|
||||||
|
}
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
|
root.render(React.createElement(MyComponent));
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`)
|
||||||
|
await page.bringToFront()
|
||||||
|
|
||||||
|
await serviceWorker.evaluate(async () => {
|
||||||
|
await globalThis.toggleExtensionForActiveTab()
|
||||||
|
})
|
||||||
|
await new Promise(r => setTimeout(r, 500))
|
||||||
|
|
||||||
|
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
||||||
|
const pages = browser.contexts()[0].pages()
|
||||||
|
const cdpPage = pages.find(p => p.url().startsWith('about:'))
|
||||||
|
expect(cdpPage).toBeDefined()
|
||||||
|
|
||||||
|
const btn = cdpPage!.locator('#react-btn')
|
||||||
|
const btnCount = await btn.count()
|
||||||
|
expect(btnCount).toBe(1)
|
||||||
|
|
||||||
|
const hasBippyBefore = await cdpPage!.evaluate(() => !!(globalThis as any).__bippy)
|
||||||
|
expect(hasBippyBefore).toBe(false)
|
||||||
|
|
||||||
|
const wsUrl = getCdpUrl({ port: TEST_PORT })
|
||||||
|
const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
|
||||||
|
|
||||||
|
const { getReactSource } = await import('./react-source.js')
|
||||||
|
const source = await getReactSource({ locator: btn, cdp: cdpSession })
|
||||||
|
|
||||||
|
const hasBippyAfter = await cdpPage!.evaluate(() => !!(globalThis as any).__bippy)
|
||||||
|
expect(hasBippyAfter).toBe(true)
|
||||||
|
|
||||||
|
const hasFiber = await btn.evaluate((el) => {
|
||||||
|
const bippy = (globalThis as any).__bippy
|
||||||
|
const fiber = bippy.getFiberFromHostInstance(el)
|
||||||
|
return !!fiber
|
||||||
|
})
|
||||||
|
expect(hasFiber).toBe(true)
|
||||||
|
|
||||||
|
const componentName = await btn.evaluate((el) => {
|
||||||
|
const bippy = (globalThis as any).__bippy
|
||||||
|
const fiber = bippy.getFiberFromHostInstance(el)
|
||||||
|
let current = fiber
|
||||||
|
while (current) {
|
||||||
|
if (bippy.isCompositeFiber(current)) {
|
||||||
|
return bippy.getDisplayName(current.type)
|
||||||
|
}
|
||||||
|
current = current.return
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
expect(componentName).toBe('MyComponent')
|
||||||
|
|
||||||
|
console.log('Component name from fiber:', componentName)
|
||||||
|
console.log('Source location (null for UMD React, works on local dev servers with JSX transform):', source)
|
||||||
|
|
||||||
|
await browser.close()
|
||||||
|
await page.close()
|
||||||
|
}, 60000)
|
||||||
})
|
})
|
||||||
|
|||||||
+11
-1
@@ -19,6 +19,7 @@ import { getCDPSessionForPage, CDPSession } from './cdp-session.js'
|
|||||||
import { Debugger } from './debugger.js'
|
import { Debugger } from './debugger.js'
|
||||||
import { Editor } from './editor.js'
|
import { Editor } from './editor.js'
|
||||||
import { getStylesForLocator, formatStylesAsText, type StylesResult } from './styles.js'
|
import { getStylesForLocator, formatStylesAsText, type StylesResult } from './styles.js'
|
||||||
|
import { getReactSource, type ReactSourceLocation } from './react-source.js'
|
||||||
|
|
||||||
class CodeExecutionTimeoutError extends Error {
|
class CodeExecutionTimeoutError extends Error {
|
||||||
constructor(timeout: number) {
|
constructor(timeout: number) {
|
||||||
@@ -80,6 +81,7 @@ interface VMContext {
|
|||||||
createEditor: (options: { cdp: CDPSession }) => Editor
|
createEditor: (options: { cdp: CDPSession }) => Editor
|
||||||
getStylesForLocator: (options: { locator: any }) => Promise<StylesResult>
|
getStylesForLocator: (options: { locator: any }) => Promise<StylesResult>
|
||||||
formatStylesAsText: (styles: StylesResult) => string
|
formatStylesAsText: (styles: StylesResult) => string
|
||||||
|
getReactSource: (options: { locator: any }) => Promise<ReactSourceLocation | null>
|
||||||
require: NodeRequire
|
require: NodeRequire
|
||||||
import: (specifier: string) => Promise<any>
|
import: (specifier: string) => Promise<any>
|
||||||
}
|
}
|
||||||
@@ -641,7 +643,8 @@ server.tool(
|
|||||||
const currentDir = path.dirname(fileURLToPath(import.meta.url))
|
const currentDir = path.dirname(fileURLToPath(import.meta.url))
|
||||||
const scriptPath = path.join(currentDir, '..', 'dist', 'selector-generator.js')
|
const scriptPath = path.join(currentDir, '..', 'dist', 'selector-generator.js')
|
||||||
const scriptContent = fs.readFileSync(scriptPath, 'utf-8')
|
const scriptContent = fs.readFileSync(scriptPath, 'utf-8')
|
||||||
await elementPage.addScriptTag({ content: scriptContent })
|
const cdp = await getCDPSession({ page: elementPage })
|
||||||
|
await cdp.send('Runtime.evaluate', { expression: scriptContent })
|
||||||
}
|
}
|
||||||
|
|
||||||
return await element.evaluate((el: any) => {
|
return await element.evaluate((el: any) => {
|
||||||
@@ -709,6 +712,11 @@ server.tool(
|
|||||||
return getStylesForLocator({ locator: options.locator, cdp })
|
return getStylesForLocator({ locator: options.locator, cdp })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getReactSourceFn = async (options: { locator: any }) => {
|
||||||
|
const cdp = await getCDPSession({ page: options.locator.page() })
|
||||||
|
return getReactSource({ locator: options.locator, cdp })
|
||||||
|
}
|
||||||
|
|
||||||
let vmContextObj: VMContextWithGlobals = {
|
let vmContextObj: VMContextWithGlobals = {
|
||||||
page,
|
page,
|
||||||
context,
|
context,
|
||||||
@@ -724,6 +732,7 @@ server.tool(
|
|||||||
createEditor,
|
createEditor,
|
||||||
getStylesForLocator: getStylesForLocatorFn,
|
getStylesForLocator: getStylesForLocatorFn,
|
||||||
formatStylesAsText,
|
formatStylesAsText,
|
||||||
|
getReactSource: getReactSourceFn,
|
||||||
resetPlaywright: async () => {
|
resetPlaywright: async () => {
|
||||||
const { page: newPage, context: newContext } = await resetConnection()
|
const { page: newPage, context: newContext } = await resetConnection()
|
||||||
|
|
||||||
@@ -742,6 +751,7 @@ server.tool(
|
|||||||
createEditor,
|
createEditor,
|
||||||
getStylesForLocator: getStylesForLocatorFn,
|
getStylesForLocator: getStylesForLocatorFn,
|
||||||
formatStylesAsText,
|
formatStylesAsText,
|
||||||
|
getReactSource: getReactSourceFn,
|
||||||
resetPlaywright: vmContextObj.resetPlaywright,
|
resetPlaywright: vmContextObj.resetPlaywright,
|
||||||
require,
|
require,
|
||||||
// TODO --experimental-vm-modules is needed to make import work in vm
|
// TODO --experimental-vm-modules is needed to make import work in vm
|
||||||
|
|||||||
@@ -158,6 +158,19 @@ you have access to some functions in addition to playwright methods:
|
|||||||
console.log(formatStylesAsText(styles));
|
console.log(formatStylesAsText(styles));
|
||||||
```
|
```
|
||||||
- `formatStylesAsText(styles)`: formats a `StylesResult` object as human-readable text. Use this to display styles in a readable format.
|
- `formatStylesAsText(styles)`: formats a `StylesResult` object as human-readable text. Use this to display styles in a readable format.
|
||||||
|
- `getReactSource({ locator })`: gets the React component source location (file, line, column) for an element.
|
||||||
|
- `locator`: a Playwright Locator or ElementHandle for the element to inspect
|
||||||
|
- Returns: `{ fileName, lineNumber, columnNumber, componentName }` or `null` if not found
|
||||||
|
- **Important**: Only works on **local dev servers** (localhost with Vite, Next.js, CRA in dev mode). Production builds strip source info. The JSX transform must have `development: true` to include `_debugSource`.
|
||||||
|
- Example:
|
||||||
|
```js
|
||||||
|
const loc = page.locator('.my-component');
|
||||||
|
const source = await getReactSource({ locator: loc });
|
||||||
|
if (source) {
|
||||||
|
console.log(`Component: ${source.componentName}`);
|
||||||
|
console.log(`File: ${source.fileName}:${source.lineNumber}:${source.columnNumber}`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
example:
|
example:
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import fs from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import type { Page, Locator, ElementHandle } from 'playwright-core'
|
||||||
|
import type { CDPSession } from './cdp-session.js'
|
||||||
|
|
||||||
|
export interface ReactSourceLocation {
|
||||||
|
fileName: string | null
|
||||||
|
lineNumber: number | null
|
||||||
|
columnNumber: number | null
|
||||||
|
componentName: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
let bippyCode: string | null = null
|
||||||
|
|
||||||
|
function getBippyCode(): string {
|
||||||
|
if (bippyCode) {
|
||||||
|
return bippyCode
|
||||||
|
}
|
||||||
|
const currentDir = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
const bippyPath = path.join(currentDir, '..', 'dist', 'bippy.js')
|
||||||
|
bippyCode = fs.readFileSync(bippyPath, 'utf-8')
|
||||||
|
return bippyCode
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getReactSource({
|
||||||
|
locator,
|
||||||
|
cdp,
|
||||||
|
}: {
|
||||||
|
locator: Locator | ElementHandle
|
||||||
|
cdp: CDPSession
|
||||||
|
}): Promise<ReactSourceLocation | null> {
|
||||||
|
const page: Page = 'page' in locator && typeof locator.page === 'function' ? locator.page() : (locator as any)._page
|
||||||
|
|
||||||
|
if (!page) {
|
||||||
|
throw new Error('Could not get page from locator')
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasBippy = await page.evaluate(() => !!(globalThis as any).__bippy)
|
||||||
|
|
||||||
|
if (!hasBippy) {
|
||||||
|
const code = getBippyCode()
|
||||||
|
await cdp.send('Runtime.evaluate', { expression: code })
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await (locator as any).evaluate(async (el: any) => {
|
||||||
|
const bippy = (globalThis as any).__bippy
|
||||||
|
if (!bippy) {
|
||||||
|
throw new Error('bippy not loaded')
|
||||||
|
}
|
||||||
|
|
||||||
|
const fiber = bippy.getFiberFromHostInstance(el)
|
||||||
|
if (!fiber) {
|
||||||
|
return { _notFound: 'fiber' as const }
|
||||||
|
}
|
||||||
|
|
||||||
|
const source = await bippy.getSource(fiber)
|
||||||
|
if (source) {
|
||||||
|
return {
|
||||||
|
fileName: source.fileName ? bippy.normalizeFileName(source.fileName) : null,
|
||||||
|
lineNumber: source.lineNumber ?? null,
|
||||||
|
columnNumber: source.columnNumber ?? null,
|
||||||
|
componentName: source.functionName ?? bippy.getDisplayName(fiber.type) ?? null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ownerStack = await bippy.getOwnerStack(fiber)
|
||||||
|
for (const frame of ownerStack) {
|
||||||
|
if (frame.fileName && bippy.isSourceFile(frame.fileName)) {
|
||||||
|
return {
|
||||||
|
fileName: bippy.normalizeFileName(frame.fileName),
|
||||||
|
lineNumber: frame.lineNumber ?? null,
|
||||||
|
columnNumber: frame.columnNumber ?? null,
|
||||||
|
componentName: frame.functionName ?? null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { _notFound: 'source' as const }
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result && '_notFound' in result) {
|
||||||
|
if (result._notFound === 'fiber') {
|
||||||
|
console.warn('[getReactSource] no fiber found - is this a React element?')
|
||||||
|
} else {
|
||||||
|
console.warn('[getReactSource] no source location found - is this a React dev build?')
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
Generated
+48
-2
@@ -38,7 +38,7 @@ importers:
|
|||||||
version: link:../playwriter
|
version: link:../playwriter
|
||||||
zustand:
|
zustand:
|
||||||
specifier: ^5.0.8
|
specifier: ^5.0.8
|
||||||
version: 5.0.8
|
version: 5.0.8(@types/react@19.2.7)(react@19.2.3)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/chrome':
|
'@types/chrome':
|
||||||
specifier: ^0.0.315
|
specifier: ^0.0.315
|
||||||
@@ -124,6 +124,9 @@ importers:
|
|||||||
'@vitest/ui':
|
'@vitest/ui':
|
||||||
specifier: ^4.0.8
|
specifier: ^4.0.8
|
||||||
version: 4.0.8(vitest@4.0.8)
|
version: 4.0.8(vitest@4.0.8)
|
||||||
|
bippy:
|
||||||
|
specifier: ^0.5.27
|
||||||
|
version: 0.5.27(@types/react@19.2.7)(react@19.2.3)
|
||||||
image-size:
|
image-size:
|
||||||
specifier: ^2.0.2
|
specifier: ^2.0.2
|
||||||
version: 2.0.2
|
version: 2.0.2
|
||||||
@@ -887,6 +890,14 @@ packages:
|
|||||||
'@types/node@24.10.1':
|
'@types/node@24.10.1':
|
||||||
resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==}
|
resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==}
|
||||||
|
|
||||||
|
'@types/react-reconciler@0.28.9':
|
||||||
|
resolution: {integrity: sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
|
||||||
|
'@types/react@19.2.7':
|
||||||
|
resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==}
|
||||||
|
|
||||||
'@types/retry@0.12.0':
|
'@types/retry@0.12.0':
|
||||||
resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==}
|
resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==}
|
||||||
|
|
||||||
@@ -1090,6 +1101,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
bippy@0.5.27:
|
||||||
|
resolution: {integrity: sha512-LvGYTQm5cMqEhnUT2tr6mSE1mrhmX/xxu3jiypHAscThhxC+9SWdaQW8Nye5CT/sR79LObNJlQpOUgi4DopTMg==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=17.0.1'
|
||||||
|
|
||||||
body-parser@2.2.0:
|
body-parser@2.2.0:
|
||||||
resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==}
|
resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -1228,6 +1244,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-OytmFH+13/QXONJcC75QNdMtKpceNk3u8ThBjyyYjkEcy/ekBwR1mMAuNvi3gdBPW3N5TlCzQ0WZw8H0lN/bDw==}
|
resolution: {integrity: sha512-OytmFH+13/QXONJcC75QNdMtKpceNk3u8ThBjyyYjkEcy/ekBwR1mMAuNvi3gdBPW3N5TlCzQ0WZw8H0lN/bDw==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
|
csstype@3.2.3:
|
||||||
|
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||||
|
|
||||||
data-uri-to-buffer@4.0.1:
|
data-uri-to-buffer@4.0.1:
|
||||||
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
|
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
|
||||||
engines: {node: '>= 12'}
|
engines: {node: '>= 12'}
|
||||||
@@ -2220,6 +2239,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==}
|
resolution: {integrity: sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==}
|
||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
|
|
||||||
|
react@19.2.3:
|
||||||
|
resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
read-yaml-file@1.1.0:
|
read-yaml-file@1.1.0:
|
||||||
resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==}
|
resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -2727,6 +2750,7 @@ packages:
|
|||||||
whatwg-encoding@3.1.1:
|
whatwg-encoding@3.1.1:
|
||||||
resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
|
resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation
|
||||||
|
|
||||||
whatwg-mimetype@4.0.0:
|
whatwg-mimetype@4.0.0:
|
||||||
resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
|
resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
|
||||||
@@ -3622,6 +3646,14 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 7.16.0
|
undici-types: 7.16.0
|
||||||
|
|
||||||
|
'@types/react-reconciler@0.28.9(@types/react@19.2.7)':
|
||||||
|
dependencies:
|
||||||
|
'@types/react': 19.2.7
|
||||||
|
|
||||||
|
'@types/react@19.2.7':
|
||||||
|
dependencies:
|
||||||
|
csstype: 3.2.3
|
||||||
|
|
||||||
'@types/retry@0.12.0': {}
|
'@types/retry@0.12.0': {}
|
||||||
|
|
||||||
'@types/uuid@10.0.0': {}
|
'@types/uuid@10.0.0': {}
|
||||||
@@ -3818,6 +3850,13 @@ snapshots:
|
|||||||
|
|
||||||
binary-extensions@2.3.0: {}
|
binary-extensions@2.3.0: {}
|
||||||
|
|
||||||
|
bippy@0.5.27(@types/react@19.2.7)(react@19.2.3):
|
||||||
|
dependencies:
|
||||||
|
'@types/react-reconciler': 0.28.9(@types/react@19.2.7)
|
||||||
|
react: 19.2.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@types/react'
|
||||||
|
|
||||||
body-parser@2.2.0:
|
body-parser@2.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
bytes: 3.1.2
|
bytes: 3.1.2
|
||||||
@@ -3989,6 +4028,8 @@ snapshots:
|
|||||||
css-tree: 3.1.0
|
css-tree: 3.1.0
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
csstype@3.2.3: {}
|
||||||
|
|
||||||
data-uri-to-buffer@4.0.1: {}
|
data-uri-to-buffer@4.0.1: {}
|
||||||
|
|
||||||
data-uri-to-buffer@6.0.2:
|
data-uri-to-buffer@6.0.2:
|
||||||
@@ -5060,6 +5101,8 @@ snapshots:
|
|||||||
iconv-lite: 0.7.0
|
iconv-lite: 0.7.0
|
||||||
unpipe: 1.0.0
|
unpipe: 1.0.0
|
||||||
|
|
||||||
|
react@19.2.3: {}
|
||||||
|
|
||||||
read-yaml-file@1.1.0:
|
read-yaml-file@1.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
@@ -5660,4 +5703,7 @@ snapshots:
|
|||||||
|
|
||||||
zod@3.25.76: {}
|
zod@3.25.76: {}
|
||||||
|
|
||||||
zustand@5.0.8: {}
|
zustand@5.0.8(@types/react@19.2.7)(react@19.2.3):
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.2.7
|
||||||
|
react: 19.2.3
|
||||||
|
|||||||
Reference in New Issue
Block a user