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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-spot",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "Dev-only DOM to React source opener for React 19, Next.js, and Turbopack.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -145,6 +145,10 @@ const INTERNAL_PATH_KEYWORDS = [
145
145
  'webpack-internal',
146
146
  'turbopack-ecmascript-runtime',
147
147
  '__turbopack_',
148
+ '[turbopack]',
149
+ 'turbopack:',
150
+ 'hmr-runtime',
151
+ 'hot-reloader',
148
152
  ];
149
153
 
150
154
  /**
@@ -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 returns project-relative paths (e.g. "src/app/page.tsx").
345
- // Convert to absolute using the configured source root so the editor
346
- // can open the file directly.
347
- if (
348
- sourcePath &&
349
- !sourcePath.startsWith('/') &&
350
- !sourcePath.startsWith('file://') &&
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('Source path is relative, resolving with sourceRoot:', {
356
- sourcePath,
357
- fsRoot,
358
- });
352
+ console.log('Resolving sourcePath with sourceRoot:', { sourcePath, fsRoot });
359
353
  }
360
354
  if (fsRoot) {
361
- sourcePath = `${fsRoot}/${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
- * 检查解析出的源码路径是否指向 React/框架运行时文件。
809
+ * 检查解析出的源码路径是否指向第三方库或框架运行时文件(非用户代码)。
810
+ *
811
+ * 这是源码导航的核心守卫:当 source map 将位置映射到 node_modules 内的
812
+ * 第三方包源码时(如 next/src/client/image-component.tsx),该路径通常:
813
+ * 1. 在磁盘上不存在(包发布时不含原始 TS 源码)
814
+ * 2. 不是用户想要到达的位置(用户想去"使用处"而非"定义处")
808
815
  *
809
- * source map 错误地将 workspace 包的代码映射到运行时文件时,
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
- return RUNTIME_SOURCE_PATTERNS.some((p) => source.includes(p));
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). */
@@ -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
- resolveAndNavigate(
612
- currentChain[idx],
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
- resolveAndNavigate(
625
- target,
646
+ // 尝试解析首选目标;若失败(如第三方库组件源码不存在),
647
+ // 沿 owner chain 向上逐个尝试,直到找到可导航的用户代码位置
648
+ resolveAndNavigateWithChainFallback(
649
+ currentChain,
626
650
  onNavigateRef.current,
627
651
  editorSchemeRef.current,
628
652
  debugRef.current