Skip to content

devtools-vite: source injection silently yields zero data-tsd-source attributes (AST child-key cache poisoned by regex literals) #523

Description

@nencodev

TanStack Devtools version

  • @tanstack/devtools-vite@0.8.5
  • transitive @tanstack/devtools-bundler-core@0.1.3
  • @tanstack/react-devtools@0.10.12 (transitive @tanstack/devtools@0.14.2)
  • Vite 8.2.2, Astro 7.2.9 (Astro drives Vite here; I could not reproduce with plain Vite)
  • Node 26.7.0, Linux

Describe the bug and the steps to reproduce it

The "Go to Source" / source inspector feature injects zero data-tsd-source
attributes. There is no browser console warning and nothing in the Vite dev
server log, so the feature just silently does nothing.

Root cause, from reading @tanstack/devtools-bundler-core/dist/esm/ast-utils.js.
getChildKeys(node) memoises the child-bearing keys of each AST node type in a
module-level childKeysCache Map, seeded from the first instance of that
type it ever sees:

for (const key in node) {
  if (key === "type" || key === "start" || key === "end") continue;
  if (typeof node[key] === "object") keys.push(key);
}

A regex literal (/abc/g) is an oxc Literal whose value is an object, so
value is cached as a child key for the type Literal. Every subsequent
string Literal then reaches:

} else if ("type" in value) callback(value);

with value being a string, and throws:

TypeError: Cannot use 'in' operator to search for 'type' in use client
    at forEachChild (.../@tanstack/devtools-bundler-core/dist/esm/ast-utils.js:31:21)

"use client" is simply the first string literal in that particular file; the
throw happens for any string literal. addSourceToJsx() wraps its whole body in
try { ... } catch (e) { return; }, so the TypeError is swallowed and the
function returns undefined — no attributes, no log, no warning.

Because the cache is module-level and shared across every file and every call,
a single regex literal anywhere in the module graph disables source injection
for every string literal parsed afterwards. That is why this reproduces in a
real dev server but not in an isolated unit test.

Minimal reproduction

Two separate node processes, same target file (any .tsx file with JSX):

import { addSourceToJsx } from '@tanstack/devtools-bundler-core'

// Case A — fresh cache: works
addSourceToJsx(heroCode, heroPath, {})   // -> transformed, N attributes injected

// Case B — parse a regex literal first, then the same file: returns undefined
addSourceToJsx('const r = /abc/g;\nexport default r;\n', regexPath, {})
addSourceToJsx(heroCode, heroPath, {})   // -> undefined

Run the two cases in separate processes. Case A injects the attributes,
Case B returns undefined.

Observed in a real app: a component that reported 36 injected attributes in
isolation injected 0 once the dev server had processed the rest of the graph.

Expected behavior

Source injection should not depend on which files were parsed earlier. And a
failure inside addSourceToJsx should at least be logged rather than swallowed,
so the feature cannot fail completely silently.

Suggested fix

Guard the non-array branch the same way the array branch already is:

- } else if ("type" in value) callback(value);
+ } else if (typeof value === "object" && "type" in value) callback(value);

With that guard, Case B injects the attributes again. A more thorough fix would
make getChildKeys key the cache on the shape of the node (or only record
keys whose value is a node or an array of nodes) and drop the bare catch.

I am carrying this as a local patch-package patch for now.

Do you intend to try to help solve this bug with your own PR?

Maybe, I'll investigate and start debugging

Terms & Code of Conduct

  • I agree to follow this project's Code of Conduct
  • I understand that if my bug cannot be reliably reproduced in a debuggable environment, it will probably not get fixed and this issue may even be closed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions