react-spot 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
package/src/core/fiber-utils.ts
CHANGED
|
@@ -341,24 +341,26 @@ async function resolveViaNextDevServer(
|
|
|
341
341
|
const { originalStackFrame } = first.value;
|
|
342
342
|
let sourcePath = originalStackFrame.file || frameInfo.url;
|
|
343
343
|
|
|
344
|
-
// Next.js
|
|
345
|
-
//
|
|
346
|
-
//
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
!sourcePath.includes('://')
|
|
352
|
-
) {
|
|
353
|
-
const fsRoot = getSourceRoot();
|
|
344
|
+
// Next.js/Turbopack 返回的路径有两种形式:
|
|
345
|
+
// 1. 纯相对路径:"src/app/page.tsx"(无前导 /)
|
|
346
|
+
// 2. 虚拟根相对路径:"/src/components/layout/AppShell.tsx"(有前导 / 但非真实绝对路径)
|
|
347
|
+
// 两种情况都需要拼接 sourceRoot 才能得到可打开的文件系统绝对路径。
|
|
348
|
+
// 真正的绝对路径特征:以 fsRoot 开头(如 /Users/...)或以 file:// 开头。
|
|
349
|
+
const fsRoot = getSourceRoot();
|
|
350
|
+
if (sourcePath && !sourcePath.includes('://')) {
|
|
354
351
|
if (debug) {
|
|
355
|
-
console.log('
|
|
356
|
-
sourcePath,
|
|
357
|
-
fsRoot,
|
|
358
|
-
});
|
|
352
|
+
console.log('Resolving sourcePath with sourceRoot:', { sourcePath, fsRoot });
|
|
359
353
|
}
|
|
360
354
|
if (fsRoot) {
|
|
361
|
-
sourcePath
|
|
355
|
+
if (sourcePath.startsWith(fsRoot)) {
|
|
356
|
+
// 已经是完整的文件系统绝对路径,无需处理
|
|
357
|
+
} else if (sourcePath.startsWith('/')) {
|
|
358
|
+
// 虚拟根相对路径(如 /src/...),拼接项目根
|
|
359
|
+
sourcePath = fsRoot + sourcePath;
|
|
360
|
+
} else {
|
|
361
|
+
// 纯相对路径(如 src/app/page.tsx),用 / 连接
|
|
362
|
+
sourcePath = `${fsRoot}/${sourcePath}`;
|
|
363
|
+
}
|
|
362
364
|
}
|
|
363
365
|
}
|
|
364
366
|
|
|
@@ -804,30 +806,59 @@ export async function resolveLocation(
|
|
|
804
806
|
}
|
|
805
807
|
|
|
806
808
|
/**
|
|
807
|
-
*
|
|
809
|
+
* 检查解析出的源码路径是否指向第三方库或框架运行时文件(非用户代码)。
|
|
810
|
+
*
|
|
811
|
+
* 这是源码导航的核心守卫:当 source map 将位置映射到 node_modules 内的
|
|
812
|
+
* 第三方包源码时(如 next/src/client/image-component.tsx),该路径通常:
|
|
813
|
+
* 1. 在磁盘上不存在(包发布时不含原始 TS 源码)
|
|
814
|
+
* 2. 不是用户想要到达的位置(用户想去"使用处"而非"定义处")
|
|
808
815
|
*
|
|
809
|
-
*
|
|
810
|
-
*
|
|
816
|
+
* 例外:pnpm workspace 链接的包会通过 symlink 直接解析到 workspace 目录,
|
|
817
|
+
* 不经过 .pnpm/ 虚拟存储,因此不会被误判。
|
|
811
818
|
*/
|
|
812
|
-
const RUNTIME_SOURCE_PATTERNS = [
|
|
813
|
-
'react-jsx-runtime',
|
|
814
|
-
'react-jsx-dev-runtime',
|
|
815
|
-
'/compiled/react/',
|
|
816
|
-
'/compiled/react-dom/',
|
|
817
|
-
'/compiled/react-server-dom',
|
|
818
|
-
'node_modules/react/cjs/',
|
|
819
|
-
'node_modules/react/dist/',
|
|
820
|
-
'node_modules/react-dom/cjs/',
|
|
821
|
-
'node_modules/react-dom/dist/',
|
|
822
|
-
'node_modules/next/dist/compiled/',
|
|
823
|
-
'node_modules/next/dist/server/',
|
|
824
|
-
'node_modules/next/dist/client/',
|
|
825
|
-
'react-reconciler',
|
|
826
|
-
'scheduler/',
|
|
827
|
-
];
|
|
828
|
-
|
|
829
819
|
export function isRuntimeSource(source: string): boolean {
|
|
830
|
-
|
|
820
|
+
// pnpm 虚拟存储中的包 → 一定是第三方依赖
|
|
821
|
+
if (source.includes('node_modules/.pnpm/')) return true;
|
|
822
|
+
|
|
823
|
+
// node_modules 内部的包源码(排除直接 symlink 到 workspace 的情况)
|
|
824
|
+
if (source.includes('/node_modules/') && !isWorkspaceLinkedPath(source)) {
|
|
825
|
+
return true;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// Turbopack / webpack 打包器内部运行时文件
|
|
829
|
+
// source map 有时会映射到这些 bundler 内部模块,它们不在磁盘上存在
|
|
830
|
+
if (source.includes('[turbopack]') || source.includes('turbopack:')) return true;
|
|
831
|
+
if (source.includes('[webpack]') || source.includes('webpack-internal:')) return true;
|
|
832
|
+
|
|
833
|
+
// Next.js 构建输出目录中的文件(_next/static/chunks/ 等),
|
|
834
|
+
// 这些是编译产物而非用户源码
|
|
835
|
+
if (source.includes('/_next/static/') || source.includes('/.next/')) return true;
|
|
836
|
+
|
|
837
|
+
return false;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* 判断一个包含 node_modules 的路径是否指向 workspace 链接的包。
|
|
842
|
+
*
|
|
843
|
+
* workspace 包的特征:路径中 node_modules 后面紧跟包名,
|
|
844
|
+
* 且该路径最终指向项目 workspace 内部(不含 .pnpm)。
|
|
845
|
+
* 此判断确保 monorepo 中自己的 packages 仍可正常导航。
|
|
846
|
+
*/
|
|
847
|
+
function isWorkspaceLinkedPath(source: string): boolean {
|
|
848
|
+
const fsRoot = getSourceRoot();
|
|
849
|
+
if (!fsRoot) return false;
|
|
850
|
+
|
|
851
|
+
// workspace 链接的路径解析后应以项目根目录开头,
|
|
852
|
+
// 且不经过 .pnpm 虚拟存储
|
|
853
|
+
if (source.startsWith(fsRoot) && !source.includes('.pnpm')) {
|
|
854
|
+
// 进一步检查:node_modules 之后的路径段是否又嵌套了 node_modules
|
|
855
|
+
// 如果有多层 node_modules,通常是第三方包的依赖
|
|
856
|
+
const afterFirstNodeModules = source.split('/node_modules/').slice(1);
|
|
857
|
+
if (afterFirstNodeModules.length <= 1) {
|
|
858
|
+
return true;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
return false;
|
|
831
862
|
}
|
|
832
863
|
|
|
833
864
|
/** Clears all caches (including the Next.js dev server availability flag). */
|
package/src/react/ReactSpot.tsx
CHANGED
|
@@ -250,6 +250,27 @@ async function resolveAndNavigate(
|
|
|
250
250
|
}
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
+
/**
|
|
254
|
+
* 沿 owner chain 逐个尝试解析源码位置并跳转。
|
|
255
|
+
*
|
|
256
|
+
* 解决第三方库组件(如 next/image)的场景:当用户点击由库组件渲染的 DOM 元素时,
|
|
257
|
+
* 该 DOM 的栈帧指向库内部实现(不存在或不可达),此时应沿 chain 向上
|
|
258
|
+
* 找到最近的用户代码位置(即使用该库组件的 JSX 所在行)。
|
|
259
|
+
*/
|
|
260
|
+
async function resolveAndNavigateWithChainFallback(
|
|
261
|
+
chain: ClickToNodeInfo[],
|
|
262
|
+
onNavigate?: ReactSpotProps['onNavigate'],
|
|
263
|
+
editorScheme?: string,
|
|
264
|
+
debug?: boolean
|
|
265
|
+
): Promise<boolean> {
|
|
266
|
+
for (const entry of chain) {
|
|
267
|
+
if (!entry.stackFrame) continue;
|
|
268
|
+
const success = await resolveAndNavigate(entry, onNavigate, editorScheme, debug);
|
|
269
|
+
if (success) return true;
|
|
270
|
+
}
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
|
|
253
274
|
export function ReactSpot({
|
|
254
275
|
onNavigate,
|
|
255
276
|
sourceRoot,
|
|
@@ -608,8 +629,9 @@ export function ReactSpot({
|
|
|
608
629
|
Promise.resolve(getClickTargetRef.current(handles)).then((targetIndex) => {
|
|
609
630
|
const idx = targetIndex ?? 0;
|
|
610
631
|
if (idx >= 0 && idx < currentChain.length) {
|
|
611
|
-
|
|
612
|
-
|
|
632
|
+
// 从选中位置开始沿 chain 向上回退,处理库组件源码不可达的情况
|
|
633
|
+
resolveAndNavigateWithChainFallback(
|
|
634
|
+
currentChain.slice(idx),
|
|
613
635
|
onNavigateRef.current,
|
|
614
636
|
editorSchemeRef.current,
|
|
615
637
|
debugRef.current
|
|
@@ -621,8 +643,10 @@ export function ReactSpot({
|
|
|
621
643
|
const transformed = applyTransformer(currentChain, chainTransformerRef.current, ctx);
|
|
622
644
|
if (transformed.length > 0) navigateFromEntry(transformed[0]);
|
|
623
645
|
} else {
|
|
624
|
-
|
|
625
|
-
|
|
646
|
+
// 尝试解析首选目标;若失败(如第三方库组件源码不存在),
|
|
647
|
+
// 沿 owner chain 向上逐个尝试,直到找到可导航的用户代码位置
|
|
648
|
+
resolveAndNavigateWithChainFallback(
|
|
649
|
+
currentChain,
|
|
626
650
|
onNavigateRef.current,
|
|
627
651
|
editorSchemeRef.current,
|
|
628
652
|
debugRef.current
|