From 32e150a8e7ecb447a50d5e6e8e57472e91500268 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 2 Oct 2026 15:15:10 +0200 Subject: [PATCH] fix(devtools-bundler-core): open source files outside cwd and end bad open-source requests addSourceToJsx strips cwd from the module path. A file outside cwd (a monorepo package, or Vite run from another directory) keeps its absolute path, and the open-source handler then prefixed cwd again. The editor got paths like /repo/apps/web/repo/packages/pkg/src/panel.tsx. The handler now uses the absolute path when the cwd-relative file does not exist and the absolute one does. A missing or malformed `source` returned without ending the response, so the request stayed pending until the browser timed out. It now answers 400. Fixes #281 Fixes #176 Refs #451 --- .changeset/open-source-outside-cwd.md | 7 +++++ .../devtools-bundler-core/src/utils.test.ts | 30 ++++++++++++++----- packages/devtools-bundler-core/src/utils.ts | 23 +++++++++----- 3 files changed, 45 insertions(+), 15 deletions(-) create mode 100644 .changeset/open-source-outside-cwd.md diff --git a/.changeset/open-source-outside-cwd.md b/.changeset/open-source-outside-cwd.md new file mode 100644 index 000000000..74526ae3d --- /dev/null +++ b/.changeset/open-source-outside-cwd.md @@ -0,0 +1,7 @@ +--- +'@tanstack/devtools-bundler-core': patch +'@tanstack/devtools-vite': patch +'@tanstack/devtools-rspack': patch +--- + +Fix "Go to Source" for files outside the current working directory, for example a package in a monorepo or Vite run from another directory. The editor got a path with the working directory added twice. Also answer a missing or malformed `source` with `400` instead of leaving the request pending. diff --git a/packages/devtools-bundler-core/src/utils.test.ts b/packages/devtools-bundler-core/src/utils.test.ts index 39d57c6de..99b3566e0 100644 --- a/packages/devtools-bundler-core/src/utils.test.ts +++ b/packages/devtools-bundler-core/src/utils.test.ts @@ -1,4 +1,5 @@ import { EventEmitter } from 'node:events' +import { resolve } from 'node:path' import { beforeEach, describe, expect, it, test, vi } from 'vitest' import { handleDevToolsRequest, @@ -200,6 +201,7 @@ function createMockReq(url?: string) { function createMockRes() { return { + statusCode: 200, setHeader: vi.fn(), write: vi.fn(), end: vi.fn(), @@ -261,20 +263,33 @@ describe('handleDevToolsRequest', () => { expect(next).not.toHaveBeenCalled() }) - it('does nothing for __tsd/open-source when source is missing', () => { + it('keeps the absolute path of a source file outside cwd', () => { + // A monorepo package, or Vite run from another directory: the injected + // source is not under cwd, so it keeps its absolute path. + const file = normalizePath(resolve(process.cwd(), '../../package.json')) + const req = createMockReq( + `/__tsd/open-source?source=${encodeURIComponent(`${file}:1:1`)}`, + ) + const res = createMockRes() + + handleDevToolsRequest(req, res as any, next as any, cb as any) + + expect(cb.mock.calls[0]?.[0].data.source).toBe(file) + }) + + it('answers 400 for __tsd/open-source when source is missing', () => { const req = createMockReq('/__tsd/open-source') const res = createMockRes() handleDevToolsRequest(req, res as any, next as any, cb as any) expect(cb).not.toHaveBeenCalled() - expect(res.setHeader).not.toHaveBeenCalled() - expect(res.write).not.toHaveBeenCalled() - expect(res.end).not.toHaveBeenCalled() + expect(res.statusCode).toBe(400) + expect(res.end).toHaveBeenCalled() expect(next).not.toHaveBeenCalled() }) - it('does nothing for __tsd/open-source when source is malformed', () => { + it('answers 400 for __tsd/open-source when source is malformed', () => { const malformed = encodeURIComponent('src/file.ts:abc:def') const req = createMockReq(`/__tsd/open-source?source=${malformed}`) const res = createMockRes() @@ -282,9 +297,8 @@ describe('handleDevToolsRequest', () => { handleDevToolsRequest(req, res as any, next as any, cb as any) expect(cb).not.toHaveBeenCalled() - expect(res.setHeader).not.toHaveBeenCalled() - expect(res.write).not.toHaveBeenCalled() - expect(res.end).not.toHaveBeenCalled() + expect(res.statusCode).toBe(400) + expect(res.end).toHaveBeenCalled() expect(next).not.toHaveBeenCalled() }) diff --git a/packages/devtools-bundler-core/src/utils.ts b/packages/devtools-bundler-core/src/utils.ts index 885820d2b..496c13f08 100644 --- a/packages/devtools-bundler-core/src/utils.ts +++ b/packages/devtools-bundler-core/src/utils.ts @@ -1,3 +1,4 @@ +import { existsSync } from 'node:fs' import fs from 'node:fs/promises' import { normalizePath } from './normalize-path' import type { IncomingMessage, ServerResponse } from 'node:http' @@ -33,13 +34,10 @@ export const handleDevToolsRequest = ( if (req.url?.includes('__tsd/open-source')) { const searchParams = new URLSearchParams(req.url.split('?')[1]) - const source = searchParams.get('source') - if (!source) { - return - } - - const parsed = parseOpenSourceParam(source) + const parsed = parseOpenSourceParam(searchParams.get('source') ?? '') if (!parsed) { + res.statusCode = 400 + res.end() return } const { file, line, column } = parsed @@ -48,7 +46,7 @@ export const handleDevToolsRequest = ( type: 'open-source', routine: 'open-source', data: { - source: file ? normalizePath(`${process.cwd()}/${file}`) : undefined, + source: file ? resolveSourceFile(file) : undefined, line, column, }, @@ -133,6 +131,17 @@ export const handleDevToolsRequest = ( }) } +/** + * Injected sources are relative to cwd. A file outside cwd (a monorepo package, + * or Vite run from another directory) keeps its absolute path instead. + */ +const resolveSourceFile = (file: string) => { + const fromCwd = normalizePath(`${process.cwd()}/${file}`) + return existsSync(fromCwd) || !existsSync(file) + ? fromCwd + : normalizePath(file) +} + export const parseOpenSourceParam = (source: string) => { // Capture everything up to the last two colon-separated numeric parts as the file. // This supports filenames that may themselves contain colons.