Skip to content

perf(vite-plugin): replace babel with oxc#407

Draft
SkandarS0 wants to merge 7 commits intoTanStack:mainfrom
SkandarS0:vite-plugin-from-babel-to-oxc
Draft

perf(vite-plugin): replace babel with oxc#407
SkandarS0 wants to merge 7 commits intoTanStack:mainfrom
SkandarS0:vite-plugin-from-babel-to-oxc

Conversation

@SkandarS0
Copy link
Copy Markdown

@SkandarS0 SkandarS0 commented Mar 27, 2026

🎯 Changes

As the title says, this PR replaces babel transformer with the oxc-parser package and its visitor:

  • AI skills have been updated too,

oxc-parser preserve the formatting of the code fed to it like semi-colons, commas...
that's why the tests have been changed to remove the formatting that babel was doing

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm test:pr.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Summary by CodeRabbit

Release Notes

  • Refactor
    • Optimized internal transformation engine powering source injection, console logging enhancement, and plugin management. All existing features continue to function as expected with no breaking changes.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 27, 2026

📝 Walkthrough

Walkthrough

Replaced Babel with Oxc for AST parsing/transforms in the devtools-vite package: core transforms (source injection, plugin injection, log enhancement, devtools removal) were rewritten to use oxc-parser and text-range edits; package deps and documentation were updated accordingly.

Changes

Cohort / File(s) Summary
Docs & artifacts
\_artifacts/domain_map.yaml, docs/architecture.md, docs/source-inspector.md, docs/vite-plugin.md, packages/devtools-vite/skills/devtools-vite-plugin/SKILL.md, packages/devtools-vite/skills/devtools-vite-plugin/references/vite-options.md, packages/devtools/skills/devtools-marketplace/SKILL.md, packages/devtools/skills/devtools-production/SKILL.md
Updated documentation and artifact references from Babel-based transforms to Oxc-based transforms; minor formatting normalization.
Package manifest
packages/devtools-vite/package.json
Removed Babel-related deps/devDeps, added oxc-parser@^0.121.0, and fixed the types path.
Babel shim removal
packages/devtools-vite/src/babel.ts
Removed module that re-exported Babel parse/traverse/generator utilities.
Source injection (runtime)
packages/devtools-vite/src/inject-source.ts, packages/devtools-vite/src/inject-source.test.ts
Rewrote JSX source-attribute injection to use oxc-parser + visitor and text edits (compute offsets, insert data-tsd-source); tests updated for changed whitespace/punctuation in generated output.
Console log enhancement
packages/devtools-vite/src/enhance-logs.ts, packages/devtools-vite/src/enhance-logs.test.ts
Replaced Babel AST mutation with oxc-parser detection + string-based insertions; control flow and error handling simplified; tests adjusted minor whitespace.
Plugin injection
packages/devtools-vite/src/inject-plugin.ts, packages/devtools-vite/src/inject-plugin.test.ts
Migrated from Babel AST edits to oxc-parser with string-based transforms (transformAndInject now accepts/returns source text), added detectDevtoolsFile, adjusted tests to use inclusion assertions and added function-plugin coverage.
Devtools removal
packages/devtools-vite/src/remove-devtools.ts, packages/devtools-vite/src/remove-devtools.test.ts
Replaced Babel parse/traverse/generate with oxc-parser + manual AST walking and text-range edits; now returns { code, map: null }; tests normalized whitespace and expectations.
Misc tests / infra
packages/devtools-vite/src/enhance-logs.test.ts, other test updates listed above
Test expectations and fixtures adjusted to match new formatting/edits from Oxc-based transforms.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 I nibble code where Babel once lay,

Oxc roots weave edits, neat and fey.
No tangled trees, just offsets bright—
A rabbit's hop, transforming night. ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description covers the main change (replacing Babel with oxc), explains the consequence for tests (formatting preservation), confirms testing was done, and checks the contributing guide. However, it does not indicate a changeset was generated despite the change affecting published code. Consider whether a changeset should be generated if this change affects published code in the devtools-vite package, as this appears to be a significant refactoring that may need versioning.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'perf(vite-plugin): replace babel with oxc' clearly and concisely summarizes the main change across the entire changeset, which replaces Babel parsing/transformation with oxc-parser throughout the vite-plugin.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

"devtools"
],
"type": "module",
"types": "dist/esm//index.d.ts",
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have also fixed this // that was there for like a decade

Copy link
Copy Markdown
Collaborator

@AlemTuzlak AlemTuzlak left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good in general, I just dont want to use any on nodes, it should be fully typesafe.

Also id prefer if we had sourcemaps for this, babel used to generate them before.

When you buld the basic react example sonda shouldnt generate any warnings as well


return {
code: applyEdits(code, edits),
map: null,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would break sonda and other tools, can we also generate sourcemaps somehow?

const devtoolsComponentNames = new Set<string>()

const transformations: Array<() => void> = []
const importDeclarations = ast.program.body.filter((node: any) =>
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make these not be any, in all thw changed files i dont want casts like these

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oww you're right I forgot to remove those

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/devtools-vite/src/enhance-logs.test.ts (1)

106-114: ⚠️ Potential issue | 🟡 Minor

Fix the console.error no-call fixture.

The test name says this covers a bare console.error, but Line 109 now uses console.log. That drops coverage for the exact member-expression path this case is supposed to protect.

🧪 Proposed fix
-        console.log
+        console.error
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/devtools-vite/src/enhance-logs.test.ts` around lines 106 - 114, The
test "it does not add enhanced console.error to console.error that is not
called" uses the wrong fixture; update the string passed to enhanceConsoleLog in
the test to include a bare "console.error" (not "console.log") so the test
actually exercises the member-expression path for console.error; locate the test
case referencing enhanceConsoleLog in enhance-logs.test.ts and replace the
console.log snippet with a console.error snippet while keeping the same filename
and timeout arguments and the expect(output).toBe(undefined) assertion.
🧹 Nitpick comments (1)
packages/devtools-vite/src/inject-source.ts (1)

124-126: Consider defensive handling for edge case.

Line 125 uses a non-null assertion (!) on the result of an optional chain. While filePath will typically be defined (since id.split('?') always returns at least one element), the assertion could mask issues if id is somehow empty.

Suggested defensive improvement
   const [filePath] = id.split('?')
-  // eslint-disable-next-line `@typescript-eslint/no-non-null-asserted-optional-chain`
-  const location = filePath?.replace(normalizePath(process.cwd()), '')!
+  if (!filePath) {
+    return
+  }
+  const location = filePath.replace(normalizePath(process.cwd()), '')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/devtools-vite/src/inject-source.ts` around lines 124 - 126, The
non-null assertion on location is unsafe; update the code around the assignment
to guard against an empty or undefined filePath (from id.split('?')[0]) instead
of using `!`. Locate the `location` assignment in inject-source.ts (the line
using `const location = filePath?.replace(normalizePath(process.cwd()), '')!`)
and change it to explicitly check filePath (e.g., if (!filePath)
return/throw/log) or provide a safe fallback value (e.g., const location =
filePath ? filePath.replace(...) : '') so you don't rely on the non-null
assertion and you handle the edge case robustly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/devtools-vite/src/enhance-logs.ts`:
- Around line 101-104: serverLogMessage and browserLogMessage hardcode the label
"LOG", so transformed console.error/console.warn/etc. calls lose their original
severity; update the prefix construction to use the original console method name
(e.g., the variable capturing the transformed call like method/type/severity)
instead of the literal "LOG", and apply the appropriate styling/color mapping
for that severity (use chalk for serverLogMessage and the %c tokens/color
strings for browserLogMessage) so console.error remains labeled and styled as
ERROR, console.warn as WARN, etc., while still including finalPath and the
open-source URL.

In `@packages/devtools-vite/src/inject-plugin.ts`:
- Around line 10-16: The devtoolsPackages array no longer contains
'@tanstack/preact-devtools', causing detectDevtoolsFile() and
findDevtoolsComponentName() to never match Preact files; restore support by
adding '@tanstack/preact-devtools' back into the devtoolsPackages constant so
the detection functions can recognize Preact devtools imports and allow
auto-injection for Preact components.

In `@packages/devtools-vite/src/remove-devtools.ts`:
- Around line 261-298: finalReferenceSet currently causes removal of any
ImportSpecifier whose local.name appears in finalReferences even if that name is
still used elsewhere in the file; update the removal logic in the loop over
importDeclarations (the code using finalReferenceSet, importSpecifiers,
removableImportSpecifiers, keptSpecifiers and buildImportText) to only remove a
specifier when you can prove the binding has no remaining references outside the
removed devtools subtree — e.g. resolve the binding for specifier.local.name in
the current AST/scope (or consult the existing reference map) and check that all
its references are contained in finalReferences before treating it as removable;
otherwise keep the specifier so imports used elsewhere are not deleted.
- Around line 311-321: The removeDevtools function currently returns map: null
and ignores the _id; update it to generate and return a proper SourceMap using
the edit positions provided by transform() so sourcemap chaining works. Inside
removeDevtools, use the incoming id (rename _id to id) as the original source
name, iterate result's edit ranges/positions (from transform() — e.g.,
result.edits/positions or whatever structure tracks start/end offsets) to build
line/column mappings and produce a valid SourceMap object (using a source-map
generator or equivalent helper used elsewhere in the repo), then return that map
instead of null along with result.code; ensure didTransform handling remains the
same and the returned map references the original id as the source.
- Around line 130-173: buildImportText currently drops TypeScript importKind
modifiers, turning type-only imports into runtime imports; fix by preserving
specifier-level and declaration-level type markers: when rendering named
specifiers, prefix any spec with spec.importKind === 'type' with "type " (e.g.
"type Foo" or "type Foo as Local"), and if defaultSpecifier or
namespaceSpecifier have importKind === 'type' or if every kept specifier is
type-only then emit the declaration-level "import type" (instead of plain
"import"); update buildImportText and the places that call it (where it's used
around lines 293-296) so rebuilt import strings preserve spec.importKind and the
declaration-level type marker when appropriate.

---

Outside diff comments:
In `@packages/devtools-vite/src/enhance-logs.test.ts`:
- Around line 106-114: The test "it does not add enhanced console.error to
console.error that is not called" uses the wrong fixture; update the string
passed to enhanceConsoleLog in the test to include a bare "console.error" (not
"console.log") so the test actually exercises the member-expression path for
console.error; locate the test case referencing enhanceConsoleLog in
enhance-logs.test.ts and replace the console.log snippet with a console.error
snippet while keeping the same filename and timeout arguments and the
expect(output).toBe(undefined) assertion.

---

Nitpick comments:
In `@packages/devtools-vite/src/inject-source.ts`:
- Around line 124-126: The non-null assertion on location is unsafe; update the
code around the assignment to guard against an empty or undefined filePath (from
id.split('?')[0]) instead of using `!`. Locate the `location` assignment in
inject-source.ts (the line using `const location =
filePath?.replace(normalizePath(process.cwd()), '')!`) and change it to
explicitly check filePath (e.g., if (!filePath) return/throw/log) or provide a
safe fallback value (e.g., const location = filePath ? filePath.replace(...) :
'') so you don't rely on the non-null assertion and you handle the edge case
robustly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2cec28e6-4e50-422b-9ef7-1d3173d41b40

📥 Commits

Reviewing files that changed from the base of the PR and between e89cff4 and b486792.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (18)
  • _artifacts/domain_map.yaml
  • docs/architecture.md
  • docs/source-inspector.md
  • docs/vite-plugin.md
  • packages/devtools-vite/package.json
  • packages/devtools-vite/skills/devtools-vite-plugin/SKILL.md
  • packages/devtools-vite/skills/devtools-vite-plugin/references/vite-options.md
  • packages/devtools-vite/src/babel.ts
  • packages/devtools-vite/src/enhance-logs.test.ts
  • packages/devtools-vite/src/enhance-logs.ts
  • packages/devtools-vite/src/inject-plugin.test.ts
  • packages/devtools-vite/src/inject-plugin.ts
  • packages/devtools-vite/src/inject-source.test.ts
  • packages/devtools-vite/src/inject-source.ts
  • packages/devtools-vite/src/remove-devtools.test.ts
  • packages/devtools-vite/src/remove-devtools.ts
  • packages/devtools/skills/devtools-marketplace/SKILL.md
  • packages/devtools/skills/devtools-production/SKILL.md
💤 Files with no reviewable changes (1)
  • packages/devtools-vite/src/babel.ts

Comment on lines +101 to +104
const serverLogMessage = `${chalk.magenta('LOG')} ${chalk.blueBright(finalPath)}\n → `
const browserLogMessage = `%cLOG%c %cGo to Source: http://localhost:${port}/__tsd/open-source?source=${encodeURIComponent(
finalPath,
)}%c \n → `
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Preserve the console.error() severity in the injected prefix.

Lines 101-104 hardcode LOG, so transformed console.error() calls are labeled as logs in both the terminal and browser output.

✏️ Proposed fix
+        const levelLabel =
+          node.callee.property.name === 'error' ? 'ERROR' : 'LOG'
+
-        const serverLogMessage = `${chalk.magenta('LOG')} ${chalk.blueBright(finalPath)}\n → `
-        const browserLogMessage = `%cLOG%c %cGo to Source: http://localhost:${port}/__tsd/open-source?source=${encodeURIComponent(
+        const serverLogMessage = `${chalk.magenta(levelLabel)} ${chalk.blueBright(finalPath)}\n → `
+        const browserLogMessage = `%c${levelLabel}%c %cGo to Source: http://localhost:${port}/__tsd/open-source?source=${encodeURIComponent(
           finalPath,
         )}%c \n → `
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/devtools-vite/src/enhance-logs.ts` around lines 101 - 104,
serverLogMessage and browserLogMessage hardcode the label "LOG", so transformed
console.error/console.warn/etc. calls lose their original severity; update the
prefix construction to use the original console method name (e.g., the variable
capturing the transformed call like method/type/severity) instead of the literal
"LOG", and apply the appropriate styling/color mapping for that severity (use
chalk for serverLogMessage and the %c tokens/color strings for
browserLogMessage) so console.error remains labeled and styled as ERROR,
console.warn as WARN, etc., while still including finalPath and the open-source
URL.

Comment on lines +130 to +173
const buildImportText = (node: any, keptSpecifiers: Array<any>) => {
const source = node.source.value

const defaultSpecifier = keptSpecifiers.find(
(spec) => spec.type === 'ImportDefaultSpecifier',
)
const namespaceSpecifier = keptSpecifiers.find(
(spec) => spec.type === 'ImportNamespaceSpecifier',
)
const namedSpecifiers = keptSpecifiers.filter(
(spec) => spec.type === 'ImportSpecifier',
)

const parts: Array<string> = []

if (defaultSpecifier) {
parts.push(defaultSpecifier.local.name)
}

if (namespaceSpecifier) {
parts.push(`* as ${namespaceSpecifier.local.name}`)
}

if (namedSpecifiers.length > 0) {
const names = namedSpecifiers.map((spec) => {
const importedName =
spec.imported?.type === 'Identifier' ? spec.imported.name : spec.local.name
const localName = spec.local.name

if (importedName === localName) {
return importedName
}

return `${importedName} as ${localName}`
})

parts.push(`{ ${names.join(', ')} }`)
}

if (parts.length === 0) {
return ''
}

return `import ${parts.join(', ')} from '${source}';`
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

❓ Verification inconclusive

Script executed:

cat -n packages/devtools-vite/src/remove-devtools.ts | head -200

Repository: TanStack/devtools


Repository: TanStack/devtools
Exit code: 0

stdout:

     1	import { parseSync, visitorKeys } from 'oxc-parser'
     2	
     3	type Edit = {
     4	  start: number
     5	  end: number
     6	  text: string
     7	}
     8	
     9	type ImportDeclarationNode = {
    10	  type: 'ImportDeclaration'
    11	  source: { value: string }
    12	  specifiers: Array<any>
    13	  start: number
    14	  end: number
    15	}
    16	
    17	const isTanStackDevtoolsImport = (source: string) =>
    18	  source === '@tanstack/react-devtools' ||
    19	  source === '@tanstack/devtools' ||
    20	  source === '@tanstack/solid-devtools'
    21	
    22	const isImportDeclarationNode = (node: any) => {
    23	  return (
    24	    node?.type === 'ImportDeclaration' &&
    25	    typeof node?.source?.value === 'string' &&
    26	    Array.isArray(node?.specifiers)
    27	  )
    28	}
    29	
    30	const getImportedNames = (importDecl: ImportDeclarationNode): Array<string> => {
    31	  return importDecl.specifiers
    32	    .map((spec: any) => spec.local?.name)
    33	    .filter((name: unknown): name is string => typeof name === 'string')
    34	}
    35	
    36	const getJsxElementName = (node: any): string | null => {
    37	  if (node.type === 'JSXIdentifier') {
    38	    return node.name
    39	  }
    40	
    41	  return null
    42	}
    43	
    44	const getLeftoverImports = (jsxElement: any) => {
    45	  const finalReferences: Array<string> = []
    46	
    47	  const openingElement = jsxElement.openingElement
    48	  if (!openingElement || !Array.isArray(openingElement.attributes)) {
    49	    return finalReferences
    50	  }
    51	
    52	  for (const attribute of openingElement.attributes) {
    53	    if (attribute.type !== 'JSXAttribute') {
    54	      continue
    55	    }
    56	
    57	    if (attribute.name.type !== 'JSXIdentifier' || attribute.name.name !== 'plugins') {
    58	      continue
    59	    }
    60	
    61	    if (
    62	      !attribute.value ||
    63	      attribute.value.type !== 'JSXExpressionContainer' ||
    64	      attribute.value.expression.type !== 'ArrayExpression'
    65	    ) {
    66	      continue
    67	    }
    68	
    69	    for (const element of attribute.value.expression.elements) {
    70	      if (!element || element.type !== 'ObjectExpression') {
    71	        continue
    72	      }
    73	
    74	      for (const prop of element.properties) {
    75	        if (
    76	          prop.type !== 'Property' ||
    77	          prop.key.type !== 'Identifier' ||
    78	          prop.key.name !== 'render'
    79	        ) {
    80	          continue
    81	        }
    82	
    83	        const value = prop.value
    84	
    85	        if (value.type === 'JSXElement') {
    86	          const elementName = getJsxElementName(value.openingElement?.name)
    87	          if (elementName) {
    88	            finalReferences.push(elementName)
    89	          }
    90	          continue
    91	        }
    92	
    93	        if (value.type === 'ArrowFunctionExpression' || value.type === 'FunctionExpression') {
    94	          const body = value.body
    95	          if (body?.type === 'JSXElement') {
    96	            const elementName = getJsxElementName(body.openingElement?.name)
    97	            if (elementName) {
    98	              finalReferences.push(elementName)
    99	            }
   100	          }
   101	          continue
   102	        }
   103	
   104	        if (value.type === 'Identifier') {
   105	          finalReferences.push(value.name)
   106	          continue
   107	        }
   108	
   109	        if (value.type === 'CallExpression' && value.callee.type === 'Identifier') {
   110	          finalReferences.push(value.callee.name)
   111	        }
   112	      }
   113	    }
   114	  }
   115	
   116	  return finalReferences
   117	}
   118	
   119	const applyEdits = (code: string, edits: Array<Edit>) => {
   120	  const ordered = [...edits].sort((a, b) => b.start - a.start)
   121	  let next = code
   122	
   123	  for (const edit of ordered) {
   124	    next = next.slice(0, edit.start) + edit.text + next.slice(edit.end)
   125	  }
   126	
   127	  return next
   128	}
   129	
   130	const buildImportText = (node: any, keptSpecifiers: Array<any>) => {
   131	  const source = node.source.value
   132	
   133	  const defaultSpecifier = keptSpecifiers.find(
   134	    (spec) => spec.type === 'ImportDefaultSpecifier',
   135	  )
   136	  const namespaceSpecifier = keptSpecifiers.find(
   137	    (spec) => spec.type === 'ImportNamespaceSpecifier',
   138	  )
   139	  const namedSpecifiers = keptSpecifiers.filter(
   140	    (spec) => spec.type === 'ImportSpecifier',
   141	  )
   142	
   143	  const parts: Array<string> = []
   144	
   145	  if (defaultSpecifier) {
   146	    parts.push(defaultSpecifier.local.name)
   147	  }
   148	
   149	  if (namespaceSpecifier) {
   150	    parts.push(`* as ${namespaceSpecifier.local.name}`)
   151	  }
   152	
   153	  if (namedSpecifiers.length > 0) {
   154	    const names = namedSpecifiers.map((spec) => {
   155	      const importedName =
   156	        spec.imported?.type === 'Identifier' ? spec.imported.name : spec.local.name
   157	      const localName = spec.local.name
   158	
   159	      if (importedName === localName) {
   160	        return importedName
   161	      }
   162	
   163	      return `${importedName} as ${localName}`
   164	    })
   165	
   166	    parts.push(`{ ${names.join(', ')} }`)
   167	  }
   168	
   169	  if (parts.length === 0) {
   170	    return ''
   171	  }
   172	
   173	  return `import ${parts.join(', ')} from '${source}';`
   174	}
   175	
   176	const getOpeningTagRootName = (name: any): string | null => {
   177	  if (name.type === 'JSXIdentifier') {
   178	    return name.name
   179	  }
   180	
   181	  if (name.type === 'JSXMemberExpression' && name.object.type === 'JSXIdentifier') {
   182	    return name.object.name
   183	  }
   184	
   185	  return null
   186	}
   187	
   188	const transform = (code: string) => {
   189	  const ast = parseSync('remove-devtools.tsx', code, {
   190	    sourceType: 'module',
   191	    lang: 'tsx',
   192	    range: true,
   193	    preserveParens: true,
   194	  })
   195	
   196	  if (ast.errors.length > 0) {
   197	    return { didTransform: false, code }
   198	  }
   199	
   200	  let didTransform = false

Script executed:

cat -n packages/devtools-vite/src/remove-devtools.ts | sed -n '200,330p'

Repository: TanStack/devtools


Repository: TanStack/devtools
Exit code: 0

stdout:

   200	  let didTransform = false
   201	  const edits: Array<Edit> = []
   202	  const finalReferences: Array<string> = []
   203	  const devtoolsComponentNames = new Set<string>()
   204	
   205	  const importDeclarations = ast.program.body.filter((node: any) =>
   206	    isImportDeclarationNode(node),
   207	  ) as Array<ImportDeclarationNode>
   208	
   209	  for (const importDecl of importDeclarations) {
   210	    const importSource = importDecl.source.value
   211	    if (!isTanStackDevtoolsImport(importSource)) {
   212	      continue
   213	    }
   214	
   215	    getImportedNames(importDecl).forEach((name: string) =>
   216	      devtoolsComponentNames.add(name),
   217	    )
   218	    edits.push({
   219	      start: importDecl.start,
   220	      end: importDecl.end,
   221	      text: '',
   222	    })
   223	    didTransform = true
   224	  }
   225	
   226	  const walk = (node: any) => {
   227	    if (!node || typeof node !== 'object' || typeof node.type !== 'string') {
   228	      return
   229	    }
   230	
   231	    if (node.type === 'JSXElement') {
   232	      const rootName = getOpeningTagRootName(node.openingElement?.name)
   233	
   234	      if (rootName && devtoolsComponentNames.has(rootName)) {
   235	        finalReferences.push(...getLeftoverImports(node))
   236	        edits.push({
   237	          start: node.start,
   238	          end: node.end,
   239	          text: '',
   240	        })
   241	        didTransform = true
   242	        return
   243	      }
   244	    }
   245	
   246	    const keys = visitorKeys[node.type] || []
   247	    for (const key of keys) {
   248	      const child = node[key]
   249	      if (Array.isArray(child)) {
   250	        for (const item of child) {
   251	          walk(item)
   252	        }
   253	      } else {
   254	        walk(child)
   255	      }
   256	    }
   257	  }
   258	
   259	  walk(ast.program)
   260	
   261	  if (finalReferences.length > 0) {
   262	    const finalReferenceSet = new Set(finalReferences)
   263	
   264	    for (const importDecl of importDeclarations) {
   265	      if (isTanStackDevtoolsImport(importDecl.source.value)) {
   266	        continue
   267	      }
   268	
   269	      const importSpecifiers = importDecl.specifiers
   270	      const removableImportSpecifiers = importSpecifiers.filter(
   271	        (specifier: any) =>
   272	          specifier.type === 'ImportSpecifier' &&
   273	          finalReferenceSet.has(specifier.local.name),
   274	      )
   275	
   276	      if (removableImportSpecifiers.length === 0) {
   277	        continue
   278	      }
   279	
   280	      const keptSpecifiers = importSpecifiers.filter(
   281	        (specifier: any) => !removableImportSpecifiers.includes(specifier),
   282	      )
   283	
   284	      if (keptSpecifiers.length === 0) {
   285	        edits.push({
   286	          start: importDecl.start,
   287	          end: importDecl.end,
   288	          text: '',
   289	        })
   290	        continue
   291	      }
   292	
   293	      edits.push({
   294	        start: importDecl.start,
   295	        end: importDecl.end,
   296	        text: buildImportText(importDecl, keptSpecifiers),
   297	      })
   298	    }
   299	  }
   300	
   301	  if (!didTransform) {
   302	    return { didTransform: false, code }
   303	  }
   304	
   305	  return {
   306	    didTransform: true,
   307	    code: applyEdits(code, edits),
   308	  }
   309	}
   310	
   311	export function removeDevtools(code: string, _id: string) {
   312	  try {
   313	    const result = transform(code)
   314	    if (!result.didTransform) {
   315	      return
   316	    }
   317	
   318	    return {
   319	      code: result.code,
   320	      map: null,
   321	    }
   322	  } catch {
   323	    return
   324	  }
   325	}

Script executed:

# Search for any tests or examples that might show import handling
rg -A 5 "import.*type" packages/devtools-vite --type ts -i

Repository: TanStack/devtools


Repository: TanStack/devtools
Exit code: 0

stdout:

packages/devtools-vite/src/virtual-console.ts:import type { ConsoleLevel } from './plugin'
packages/devtools-vite/src/virtual-console.ts-
packages/devtools-vite/src/virtual-console.ts-// export const VIRTUAL_MODULE_ID = 'virtual:tanstack-devtools-console'
packages/devtools-vite/src/virtual-console.ts-// export const RESOLVED_VIRTUAL_MODULE_ID = '\0' + VIRTUAL_MODULE_ID
packages/devtools-vite/src/virtual-console.ts-
packages/devtools-vite/src/virtual-console.ts-/**
--
packages/devtools-vite/src/utils.ts:import type { Connect } from 'vite'
packages/devtools-vite/src/utils.ts:import type { IncomingMessage, ServerResponse } from 'node:http'
packages/devtools-vite/src/utils.ts:import type { PackageJson } from '@tanstack/devtools-client'
packages/devtools-vite/src/utils.ts-
packages/devtools-vite/src/utils.ts-type DevToolsRequestHandler = (data: any) => void
packages/devtools-vite/src/utils.ts-
packages/devtools-vite/src/utils.ts-type DevToolsViteRequestOptions = {
packages/devtools-vite/src/utils.ts-  onOpenSource?: DevToolsRequestHandler
--
packages/devtools-vite/tests/index.test.ts:import type { Plugin } from 'vite'
packages/devtools-vite/tests/index.test.ts-
packages/devtools-vite/tests/index.test.ts-// Helper to find a plugin by name from the array returned by devtools()
packages/devtools-vite/tests/index.test.ts-function findPlugin(plugins: Array<Plugin>, name: string): Plugin | undefined {
packages/devtools-vite/tests/index.test.ts-  return plugins.find((p) => p.name === name)
packages/devtools-vite/tests/index.test.ts-}
--
packages/devtools-vite/src/remove-devtools.ts:        spec.imported?.type === 'Identifier' ? spec.imported.name : spec.local.name
packages/devtools-vite/src/remove-devtools.ts-      const localName = spec.local.name
packages/devtools-vite/src/remove-devtools.ts-
packages/devtools-vite/src/remove-devtools.ts-      if (importedName === localName) {
packages/devtools-vite/src/remove-devtools.ts-        return importedName
packages/devtools-vite/src/remove-devtools.ts-      }
--
packages/devtools-vite/src/package-manager.ts:import type { OutdatedDeps } from '@tanstack/devtools-client'
packages/devtools-vite/src/package-manager.ts-
packages/devtools-vite/src/package-manager.ts-/**
packages/devtools-vite/src/package-manager.ts- * Gets the outdated command for the detected package manager
packages/devtools-vite/src/package-manager.ts- */
packages/devtools-vite/src/package-manager.ts-const getOutdatedCommand = (packageManager: string): string => {
--
packages/devtools-vite/src/package-manager.ts:  pluginImport?: { importName: string; type: 'jsx' | 'function' },
packages/devtools-vite/src/package-manager.ts-): { success: boolean; error?: string } => {
packages/devtools-vite/src/package-manager.ts-  // Check if we found the devtools file
packages/devtools-vite/src/package-manager.ts-  if (!devtoolsFileId) {
packages/devtools-vite/src/package-manager.ts-    const error = 'Devtools file not found'
packages/devtools-vite/src/package-manager.ts-    console.log(
--
packages/devtools-vite/src/plugin.ts:import type { ServerResponse } from 'node:http'
packages/devtools-vite/src/plugin.ts:import type { Plugin } from 'vite'
packages/devtools-vite/src/plugin.ts:import type { EditorConfig } from './editor'
packages/devtools-vite/src/plugin.ts:import type {
packages/devtools-vite/src/plugin.ts-  HttpServerLike,
packages/devtools-vite/src/plugin.ts-  ServerEventBusConfig,
packages/devtools-vite/src/plugin.ts-} from '@tanstack/devtools-event-bus/server'
packages/devtools-vite/src/plugin.ts-
packages/devtools-vite/src/plugin.ts-export type ConsoleLevel = 'log' | 'warn' | 'error' | 'info' | 'debug'
--
packages/devtools-vite/src/enhance-logs.ts:import type { CallExpression, MemberExpression } from 'oxc-parser'
packages/devtools-vite/src/enhance-logs.ts-
packages/devtools-vite/src/enhance-logs.ts-type Insertion = {
packages/devtools-vite/src/enhance-logs.ts-  at: number
packages/devtools-vite/src/enhance-logs.ts-  text: string
packages/devtools-vite/src/enhance-logs.ts-}
--
packages/devtools-vite/src/inject-plugin.ts:import type { PluginInjection } from '@tanstack/devtools-client'
packages/devtools-vite/src/inject-plugin.ts-
packages/devtools-vite/src/inject-plugin.ts-type Edit = {
packages/devtools-vite/src/inject-plugin.ts-  at: number
packages/devtools-vite/src/inject-plugin.ts-  text: string
packages/devtools-vite/src/inject-plugin.ts-}
--
packages/devtools-vite/src/inject-plugin.ts:            spec.imported.type === 'Identifier' &&
packages/devtools-vite/src/inject-plugin.ts-            spec.imported.name === 'TanStackDevtools',
packages/devtools-vite/src/inject-plugin.ts-        )
packages/devtools-vite/src/inject-plugin.ts-
packages/devtools-vite/src/inject-plugin.ts:        if (namedImport && namedImport.type === 'ImportSpecifier') {
packages/devtools-vite/src/inject-plugin.ts-          componentName = namedImport.local.name
packages/devtools-vite/src/inject-plugin.ts-          return
packages/devtools-vite/src/inject-plugin.ts-        }
packages/devtools-vite/src/inject-plugin.ts-
packages/devtools-vite/src/inject-plugin.ts-        const namespaceImport = node.specifiers.find(
--
packages/devtools-vite/src/inject-plugin.ts:          namespaceImport.type === 'ImportNamespaceSpecifier'
packages/devtools-vite/src/inject-plugin.ts-        ) {
packages/devtools-vite/src/inject-plugin.ts-          componentName = `${namespaceImport.local.name}.TanStackDevtools`
packages/devtools-vite/src/inject-plugin.ts-        }
packages/devtools-vite/src/inject-plugin.ts-      },
packages/devtools-vite/src/inject-plugin.ts-    }).visit(result.program)
--
packages/devtools-vite/src/inject-plugin.ts:  const pluginType = injection.pluginImport?.type || 'jsx'
packages/devtools-vite/src/inject-plugin.ts-  const displayName = injection.pluginName
packages/devtools-vite/src/inject-plugin.ts-
packages/devtools-vite/src/inject-plugin.ts-  if (!importName) {
packages/devtools-vite/src/inject-plugin.ts-    return { didTransform: false, code }
packages/devtools-vite/src/inject-plugin.ts-  }
--
packages/devtools-vite/src/inject-plugin.ts:    const imports = ast.program.body.filter((node: any) => node.type === 'ImportDeclaration')
packages/devtools-vite/src/inject-plugin.ts-    const lastImport = imports[imports.length - 1]
packages/devtools-vite/src/inject-plugin.ts-    const importAt = lastImport ? lastImport.end : 0
packages/devtools-vite/src/inject-plugin.ts-
packages/devtools-vite/src/inject-plugin.ts-    const importText = `\nimport { ${importName} } from ${JSON.stringify(injection.packageName)};`
packages/devtools-vite/src/inject-plugin.ts-    edits.push({ at: importAt, text: importText })
--
packages/devtools-vite/src/inject-plugin.test.ts:        import type { FC } from 'react'
packages/devtools-vite/src/inject-plugin.test.ts-
packages/devtools-vite/src/inject-plugin.test.ts-        const App: FC = () => {
packages/devtools-vite/src/inject-plugin.test.ts-          return <TanStackDevtools plugins={[]} />
packages/devtools-vite/src/inject-plugin.test.ts-        }
packages/devtools-vite/src/inject-plugin.test.ts-      `
--
packages/devtools-vite/src/inject-plugin.test.ts:        'import type { FC } from \'react\'',
packages/devtools-vite/src/inject-plugin.test.ts-        '{ name: "TanStack Query", render: <ReactQueryDevtoolsPanel /> }',
packages/devtools-vite/src/inject-plugin.test.ts-      ])
packages/devtools-vite/src/inject-plugin.test.ts-    })
packages/devtools-vite/src/inject-plugin.test.ts-
packages/devtools-vite/src/inject-plugin.test.ts-    test('should handle plugins array with trailing comma', () => {

Script executed:

# Search for test cases related to remove-devtools to see if type imports are tested
rg -B 2 -A 10 "type.*import|import.*type" packages/devtools-vite/src/remove-devtools.test.ts 2>/dev/null || echo "No test file found for remove-devtools"

Repository: TanStack/devtools


Repository: TanStack/devtools
Exit code: 0

stdout:

No test file found for remove-devtools

Script executed:

# Check if there's a test file for remove-devtools
find packages/devtools-vite -name "*remove-devtools*test*" -o -name "*test*remove-devtools*"

Repository: TanStack/devtools


Repository: TanStack/devtools
Exit code: 0

stdout:

packages/devtools-vite/src/remove-devtools.test.ts

Script executed:

cat -n packages/devtools-vite/src/remove-devtools.test.ts

Repository: TanStack/devtools


Repository: TanStack/devtools
Exit code: 0

stdout:

     1	import { describe, expect, test } from 'vitest'
     2	import { removeDevtools } from './remove-devtools'
     3	
     4	const removeEmptySpace = (str: string) => {
     5	  return str.replace(/\s/g, '').trim()
     6	}
     7	
     8	describe('remove-devtools', () => {
     9	  test('it removes devtools if Imported directly', () => {
    10	    const output = removeEmptySpace(
    11	      removeDevtools(
    12	        `
    13	      import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools'
    14	import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'
    15	import {
    16	  Link,
    17	  Outlet,
    18	  RouterProvider,
    19	  createRootRoute,
    20	  createRoute,
    21	  createRouter,
    22	} from '@tanstack/react-router'
    23	import { TanStackDevtools } from '@tanstack/react-devtools'
    24	
    25	
    26	
    27	export default function DevtoolsExample() {
    28	  return (
    29	    <>
    30	      <TanStackDevtools
    31	        eventBusConfig={{
    32	          connectToServerBus: true,
    33	        }}
    34	        plugins={[
    35	          {
    36	            name: 'TanStack Query',
    37	            render: <ReactQueryDevtoolsPanel />,
    38	          },
    39	          {
    40	            name: 'TanStack Router',
    41	            render: <TanStackRouterDevtoolsPanel router={router} />,
    42	          },
    43	          /* {
    44	            name: "The actual app",
    45	            render: <iframe style={{ width: '100%', height: '100%' }} src="http://localhost:3005" />,
    46	          } */
    47	        ]}
    48	      />
    49	      <RouterProvider router={router} />
    50	    </>
    51	  )
    52	}
    53	
    54	        `,
    55	        'test.jsx',
    56	      )!.code,
    57	    )
    58	    expect(output).toBe(
    59	      removeEmptySpace(`
    60	               import {
    61	            Link,
    62	            Outlet,
    63	            RouterProvider,
    64	            createRootRoute,
    65	            createRoute,
    66	            createRouter,
    67	          } from '@tanstack/react-router'
    68	
    69	
    70	          export default function DevtoolsExample() {
    71	            return  (<>
    72	                <RouterProvider router={router} />
    73	              </>)
    74	
    75	          }
    76	
    77	        `),
    78	    )
    79	  })
    80	
    81	  test("it removes devtools if Imported and renamed with 'as' ", () => {
    82	    const output = removeEmptySpace(
    83	      removeDevtools(
    84	        `
    85	      import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools'
    86	import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'
    87	import {
    88	  Link,
    89	  Outlet,
    90	  RouterProvider,
    91	  createRootRoute,
    92	  createRoute,
    93	  createRouter,
    94	} from '@tanstack/react-router'
    95	import { TanStackDevtools as Devtools } from '@tanstack/react-devtools'
    96	
    97	
    98	
    99	export default function DevtoolsExample() {
   100	  return (
   101	    <>
   102	      <Devtools
   103	        eventBusConfig={{
   104	          connectToServerBus: true,
   105	        }}
   106	        plugins={[
   107	          {
   108	            name: 'TanStack Query',
   109	            render: <ReactQueryDevtoolsPanel />,
   110	          },
   111	          {
   112	            name: 'TanStack Router',
   113	            render: <TanStackRouterDevtoolsPanel router={router} />,
   114	          },
   115	          /* {
   116	            name: "The actual app",
   117	            render: <iframe style={{ width: '100%', height: '100%' }} src="http://localhost:3005" />,
   118	          } */
   119	        ]}
   120	      />
   121	      <RouterProvider router={router} />
   122	    </>
   123	  )
   124	}
   125	
   126	        `,
   127	        'test.jsx',
   128	      )!.code,
   129	    )
   130	    expect(output).toBe(
   131	      removeEmptySpace(`
   132	              import {
   133	            Link,
   134	            Outlet,
   135	            RouterProvider,
   136	            createRootRoute,
   137	            createRoute,
   138	            createRouter,
   139	          } from '@tanstack/react-router'
   140	
   141	
   142	          export default function DevtoolsExample() {
   143	            return ( <>
   144	                <RouterProvider router={router} />
   145	              </>)
   146	
   147	          }
   148	
   149	        `),
   150	    )
   151	  })
   152	
   153	  test('it removes devtools if Imported as * then used as a subcomponent ', () => {
   154	    const output = removeEmptySpace(
   155	      removeDevtools(
   156	        `
   157	      import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools'
   158	import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'
   159	import {
   160	  Link,
   161	  Outlet,
   162	  RouterProvider,
   163	  createRootRoute,
   164	  createRoute,
   165	  createRouter,
   166	} from '@tanstack/react-router'
   167	import * as Tools from '@tanstack/react-devtools'
   168	
   169	
   170	
   171	export default function DevtoolsExample() {
   172	  return (
   173	    <>
   174	      <Tools.TanStackDevtools
   175	        eventBusConfig={{
   176	          connectToServerBus: true,
   177	        }}
   178	        plugins={[
   179	          {
   180	            name: 'TanStack Query',
   181	            render: <ReactQueryDevtoolsPanel />,
   182	          },
   183	          {
   184	            name: 'TanStack Router',
   185	            render: <TanStackRouterDevtoolsPanel router={router} />,
   186	          },
   187	          /* {
   188	            name: "The actual app",
   189	            render: <iframe style={{ width: '100%', height: '100%' }} src="http://localhost:3005" />,
   190	          } */
   191	        ]}
   192	      />
   193	      <RouterProvider router={router} />
   194	    </>
   195	  )
   196	}
   197	
   198	        `,
   199	        'test.jsx',
   200	      )!.code,
   201	    )
   202	    expect(output).toBe(
   203	      removeEmptySpace(`
   204	            import {
   205	            Link,
   206	            Outlet,
   207	            RouterProvider,
   208	            createRootRoute,
   209	            createRoute,
   210	            createRouter,
   211	          } from '@tanstack/react-router'
   212	
   213	
   214	          export default function DevtoolsExample() {
   215	            return  (<>
   216	                <RouterProvider router={router} />
   217	              </>)
   218	
   219	          }
   220	
   221	        `),
   222	    )
   223	  })
   224	
   225	  test('it removes devtools and all possible variations of the plugins', () => {
   226	    const output = removeEmptySpace(
   227	      removeDevtools(
   228	        `
   229	      import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools'
   230	import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'
   231	import {
   232	  Link,
   233	  Outlet,
   234	  RouterProvider,
   235	  createRootRoute,
   236	  createRoute,
   237	  createRouter,
   238	} from '@tanstack/react-router'
   239	import * as Tools from '@tanstack/react-devtools'
   240	
   241	
   242	
   243	export default function DevtoolsExample() {
   244	  return (
   245	    <>
   246	      <Tools.TanStackDevtools
   247	        eventBusConfig={{
   248	          connectToServerBus: true,
   249	        }}
   250	        plugins={[
   251	          {
   252	            name: 'TanStack Query',
   253	            render: <ReactQueryDevtoolsPanel />,
   254	          },
   255	                 {
   256	            name: 'TanStack Query',
   257	            render: () => <ReactQueryDevtoolsPanel />,
   258	          },
   259	          {
   260	            name: 'TanStack Router',
   261	            render: TanStackRouterDevtoolsPanel,
   262	          },
   263	          some()
   264	        ]}
   265	      />
   266	      <RouterProvider router={router} />
   267	    </>
   268	  )
   269	}
   270	`,
   271	        'test.jsx',
   272	      )!.code,
   273	    )
   274	
   275	    expect(output).toBe(
   276	      removeEmptySpace(`
   277	import {
   278	  Link,
   279	  Outlet,
   280	  RouterProvider,
   281	  createRootRoute,
   282	  createRoute,
   283	  createRouter,
   284	} from '@tanstack/react-router'
   285	
   286	
   287	
   288	export default function DevtoolsExample() {
   289	  return (
   290	    <>
   291	      <RouterProvider router={router} />
   292	    </>
   293	  )
   294	}
   295	          `),
   296	    )
   297	  })
   298	
   299	  describe('removing plugin imports', () => {
   300	    test('it removes the plugin import from the import array if multiple import identifiers exist', () => {
   301	      const output = removeEmptySpace(
   302	        removeDevtools(
   303	          `
   304	      import { ReactQueryDevtoolsPanel, test } from '@tanstack/react-query-devtools'
   305	
   306	import * as Tools from '@tanstack/react-devtools'
   307	
   308	
   309	
   310	export default function DevtoolsExample() {
   311	  return (
   312	    <>
   313	      <Tools.TanStackDevtools
   314	        eventBusConfig={{
   315	          connectToServerBus: true,
   316	        }}
   317	        plugins={[
   318	          {
   319	            name: 'TanStack Query',
   320	            render: <ReactQueryDevtoolsPanel />,
   321	          }
   322	        ]}
   323	      />
   324	      <RouterProvider router={router} />
   325	    </>
   326	  )
   327	}
   328	`,
   329	          'test.jsx',
   330	        )!.code,
   331	      )
   332	
   333	      expect(output).toBe(
   334	        removeEmptySpace(`
   335	   import { test } from '@tanstack/react-query-devtools';
   336	
   337	export default function DevtoolsExample() {
   338	  return (
   339	    <>
   340	      <RouterProvider router={router} />
   341	    </>
   342	  )
   343	}
   344	          `),
   345	      )
   346	    })
   347	
   348	    test("it doesn't remove the whole import if imported with * as", () => {
   349	      const output = removeEmptySpace(
   350	        removeDevtools(
   351	          `
   352	      import * as Stuff from '@tanstack/react-query-devtools'
   353	
   354	import * as Tools from '@tanstack/react-devtools'
   355	
   356	
   357	
   358	export default function DevtoolsExample() {
   359	  return (
   360	    <>
   361	      <Tools.TanStackDevtools
   362	        eventBusConfig={{
   363	          connectToServerBus: true,
   364	        }}
   365	        plugins={[
   366	          {
   367	            name: 'TanStack Query',
   368	            render: <Stuff.ReactQueryDevtoolsPanel />,
   369	          }
   370	        ]}
   371	      />
   372	      <RouterProvider router={router} />
   373	    </>
   374	  )
   375	}
   376	`,
   377	          'test.jsx',
   378	        )!.code,
   379	      )
   380	
   381	      expect(output).toBe(
   382	        removeEmptySpace(`
   383	   import * as Stuff from '@tanstack/react-query-devtools'
   384	
   385	export default function DevtoolsExample() {
   386	  return (
   387	    <>
   388	      <RouterProvider router={router} />
   389	    </>
   390	  )
   391	}
   392	          `),
   393	      )
   394	    })
   395	
   396	    test('it removes the import completely if nothing is left', () => {
   397	      const output = removeEmptySpace(
   398	        removeDevtools(
   399	          `
   400	      import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools'
   401	import * as Tools from '@tanstack/react-devtools'
   402	
   403	export default function DevtoolsExample() {
   404	  return (
   405	    <>
   406	      <Tools.TanStackDevtools
   407	        eventBusConfig={{
   408	          connectToServerBus: true,
   409	        }}
   410	        plugins={[
   411	          {
   412	            name: 'TanStack Query',
   413	            render: <ReactQueryDevtoolsPanel />,
   414	          }
   415	        ]}
   416	      />
   417	      <RouterProvider router={router} />
   418	    </>
   419	  )
   420	}
   421	`,
   422	          'test.jsx',
   423	        )!.code,
   424	      )
   425	
   426	      expect(output).toBe(
   427	        removeEmptySpace(`
   428	export default function DevtoolsExample() {
   429	  return (
   430	    <>
   431	      <RouterProvider router={router} />
   432	    </>
   433	  )
   434	}
   435	          `),
   436	      )
   437	    })
   438	
   439	    test('it removes the import completely even if used as a function instead of jsx', () => {
   440	      const output = removeEmptySpace(
   441	        removeDevtools(
   442	          `
   443	      import { plugin } from '@tanstack/react-query-devtools'
   444	import * as Tools from '@tanstack/react-devtools'
   445	
   446	export default function DevtoolsExample() {
   447	  return (
   448	    <>
   449	      <Tools.TanStackDevtools
   450	        eventBusConfig={{
   451	          connectToServerBus: true,
   452	        }}
   453	        plugins={[
   454	          {
   455	            name: 'TanStack Query',
   456	            render: plugin()
   457	          }
   458	        ]}
   459	      />
   460	      <RouterProvider router={router} />
   461	    </>
   462	  )
   463	}
   464	`,
   465	          'test.jsx',
   466	        )!.code,
   467	      )
   468	
   469	      expect(output).toBe(
   470	        removeEmptySpace(`
   471	export default function DevtoolsExample() {
   472	  return (
   473	    <>
   474	      <RouterProvider router={router} />
   475	    </>
   476	  )
   477	}
   478	          `),
   479	      )
   480	    })
   481	
   482	    test('it removes the import completely even if used as a function inside of render', () => {
   483	      const output = removeEmptySpace(
   484	        removeDevtools(
   485	          `
   486	      import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools'
   487	import * as Tools from '@tanstack/react-devtools'
   488	
   489	export default function DevtoolsExample() {
   490	  return (
   491	    <>
   492	      <Tools.TanStackDevtools
   493	        eventBusConfig={{
   494	          connectToServerBus: true,
   495	        }}
   496	        plugins={[
   497	          {
   498	            name: 'TanStack Query',
   499	            render: () => <ReactQueryDevtoolsPanel />
   500	          }
   501	        ]}
   502	      />
   503	      <RouterProvider router={router} />
   504	    </>
   505	  )
   506	}
   507	`,
   508	          'test.jsx',
   509	        )!.code,
   510	      )
   511	
   512	      expect(output).toBe(
   513	        removeEmptySpace(`
   514	export default function DevtoolsExample() {
   515	  return (
   516	    <>
   517	      <RouterProvider router={router} />
   518	    </>
   519	  )
   520	}
   521	          `),
   522	      )
   523	    })
   524	
   525	    test('it removes the import completely even if used as a reference inside of render', () => {
   526	      const output = removeEmptySpace(
   527	        removeDevtools(
   528	          `
   529	      import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools'
   530	import * as Tools from '@tanstack/react-devtools'
   531	
   532	export default function DevtoolsExample() {
   533	  return (
   534	    <>
   535	      <Tools.TanStackDevtools
   536	        eventBusConfig={{
   537	          connectToServerBus: true,
   538	        }}
   539	        plugins={[
   540	          {
   541	            name: 'TanStack Query',
   542	            render: ReactQueryDevtoolsPanel
   543	          }
   544	        ]}
   545	      />
   546	      <RouterProvider router={router} />
   547	    </>
   548	  )
   549	}
   550	`,
   551	          'test.jsx',
   552	        )!.code,
   553	      )
   554	
   555	      expect(output).toBe(
   556	        removeEmptySpace(`
   557	export default function DevtoolsExample() {
   558	  return (
   559	    <>
   560	      <RouterProvider router={router} />
   561	    </>
   562	  )
   563	}
   564	          `),
   565	      )
   566	    })
   567	  })
   568	})

Script executed:

# Check if oxc-parser is used elsewhere and how it handles type imports
rg "ImportSpecifier|type.*import" packages/devtools-vite/src --type ts -B 2 -A 2

Repository: TanStack/devtools


Repository: TanStack/devtools
Exit code: 0

stdout:

packages/devtools-vite/src/remove-devtools.ts-  )
packages/devtools-vite/src/remove-devtools.ts-  const namedSpecifiers = keptSpecifiers.filter(
packages/devtools-vite/src/remove-devtools.ts:    (spec) => spec.type === 'ImportSpecifier',
packages/devtools-vite/src/remove-devtools.ts-  )
packages/devtools-vite/src/remove-devtools.ts-
--
packages/devtools-vite/src/remove-devtools.ts-    const names = namedSpecifiers.map((spec) => {
packages/devtools-vite/src/remove-devtools.ts-      const importedName =
packages/devtools-vite/src/remove-devtools.ts:        spec.imported?.type === 'Identifier' ? spec.imported.name : spec.local.name
packages/devtools-vite/src/remove-devtools.ts-      const localName = spec.local.name
packages/devtools-vite/src/remove-devtools.ts-
--
packages/devtools-vite/src/remove-devtools.ts-
packages/devtools-vite/src/remove-devtools.ts-      const importSpecifiers = importDecl.specifiers
packages/devtools-vite/src/remove-devtools.ts:      const removableImportSpecifiers = importSpecifiers.filter(
packages/devtools-vite/src/remove-devtools.ts-        (specifier: any) =>
packages/devtools-vite/src/remove-devtools.ts:          specifier.type === 'ImportSpecifier' &&
packages/devtools-vite/src/remove-devtools.ts-          finalReferenceSet.has(specifier.local.name),
packages/devtools-vite/src/remove-devtools.ts-      )
packages/devtools-vite/src/remove-devtools.ts-
packages/devtools-vite/src/remove-devtools.ts:      if (removableImportSpecifiers.length === 0) {
packages/devtools-vite/src/remove-devtools.ts-        continue
packages/devtools-vite/src/remove-devtools.ts-      }
packages/devtools-vite/src/remove-devtools.ts-
packages/devtools-vite/src/remove-devtools.ts-      const keptSpecifiers = importSpecifiers.filter(
packages/devtools-vite/src/remove-devtools.ts:        (specifier: any) => !removableImportSpecifiers.includes(specifier),
packages/devtools-vite/src/remove-devtools.ts-      )
packages/devtools-vite/src/remove-devtools.ts-
--
packages/devtools-vite/src/inject-plugin.ts-        const namedImport = node.specifiers.find(
packages/devtools-vite/src/inject-plugin.ts-          (spec: any) =>
packages/devtools-vite/src/inject-plugin.ts:            spec.type === 'ImportSpecifier' &&
packages/devtools-vite/src/inject-plugin.ts-            spec.imported.type === 'Identifier' &&
packages/devtools-vite/src/inject-plugin.ts-            spec.imported.name === 'TanStackDevtools',
packages/devtools-vite/src/inject-plugin.ts-        )
packages/devtools-vite/src/inject-plugin.ts-
packages/devtools-vite/src/inject-plugin.ts:        if (namedImport && namedImport.type === 'ImportSpecifier') {
packages/devtools-vite/src/inject-plugin.ts-          componentName = namedImport.local.name
packages/devtools-vite/src/inject-plugin.ts-          return
--
packages/devtools-vite/src/inject-plugin.ts-    return node.specifiers.some(
packages/devtools-vite/src/inject-plugin.ts-      (spec: any) =>
packages/devtools-vite/src/inject-plugin.ts:        spec.type === 'ImportSpecifier' && spec.local.name === importName,
packages/devtools-vite/src/inject-plugin.ts-    )
packages/devtools-vite/src/inject-plugin.ts-  })

Script executed:

# Check for oxc-parser type definitions or documentation
find . -name "*.ts" -o -name "*.d.ts" | xargs rg "oxc-parser" 2>/dev/null | head -20

Repository: TanStack/devtools


Repository: TanStack/devtools
Exit code: 0

stdout:

./packages/devtools-vite/src/remove-devtools.ts:import { parseSync, visitorKeys } from 'oxc-parser'
./packages/devtools-vite/src/inject-plugin.ts:import { Visitor, parseSync } from 'oxc-parser'
./packages/devtools-vite/src/inject-source.ts:import { parseSync, visitorKeys } from 'oxc-parser'
./packages/devtools-vite/src/enhance-logs.ts:import { Visitor, parseSync } from 'oxc-parser'
./packages/devtools-vite/src/enhance-logs.ts:import type { CallExpression, MemberExpression } from 'oxc-parser'

Script executed:

# Look for how oxc-parser handles type imports - check if there's any usage with type modifier
rg "ImportSpecifier.*type|type.*ImportSpecifier" packages/devtools-vite --type ts

Repository: TanStack/devtools


Repository: TanStack/devtools
Exit code: 0

stdout:

packages/devtools-vite/src/remove-devtools.ts:    (spec) => spec.type === 'ImportSpecifier',
packages/devtools-vite/src/remove-devtools.ts:          specifier.type === 'ImportSpecifier' &&
packages/devtools-vite/src/inject-plugin.ts:            spec.type === 'ImportSpecifier' &&
packages/devtools-vite/src/inject-plugin.ts:        if (namedImport && namedImport.type === 'ImportSpecifier') {
packages/devtools-vite/src/inject-plugin.ts:        spec.type === 'ImportSpecifier' && spec.local.name === importName,

Script executed:

# Search for documentation or examples that show oxc-parser's AST structure
web_search oxc-parser ImportSpecifier type modifier TypeScript AST

Repository: TanStack/devtools


Repository: TanStack/devtools
Exit code: 127

stderr:

/bin/bash: line 2: web_search: command not found

oxc-parser ImportSpecifier type modifier TypeScript AST structure


In oxc-parser, the JavaScript/TypeScript parser from the Oxc project, the AST follows the ESTree spec for JS/JSX and TS-ESTree (@typescript-eslint/typescript-estree) format for TypeScript (use astType: 'ts' option for TS properties in JS files too). ImportSpecifier represents named imports in ImportDeclaration.specifiers array, e.g., import { foo } from 'mod'; or import { type Foo } from 'mod';. From @oxc-project/types (npm package providing TS types) and internal Rust AST (oxc_ast): Rust AST (internal, mirrored in JS types): pub struct ImportSpecifier<'a> { span: Span, imported: ModuleExportName<'a>, // imported name (IdentifierName or StringLiteral) local: BindingIdentifier<'a>, // local binding name import_kind: ImportOrExportKind, // Value | Type (for 'type' modifier) } In the JS/TS-ESTree AST output: - type: 'ImportSpecifier' - imported: { type: 'Identifier' | 'Literal', ... } // imported name - local: { type: 'Identifier', ... } // local name - importKind?: 'type' | 'value' // TypeScript modifier ('type' keyword); absent or 'value' in JS/ESTree The 'type' modifier on ImportSpecifier (import type { Foo }) sets importKind: 'type', distinguishing type-only imports. This aligns with TS-ESTree, where ImportSpecifier has optional importKind: 'type'. No explicit 'modifiers' array on ImportSpecifier (unlike some nodes); the modifier is directly on importKind. Sources confirm ESTree/TS-ESTree conformance, with type-only import support via importKind (e.g., GitHub issues/PRs #2854, #5184, #6352 discuss importKind as string, type modifiers). Use @oxc-project/types for full TS definitions. Example parsing: import { parseSync } from 'oxc-parser'; const ast = parseSync('file.ts', import { type Foo } from 'mod';, { lang: 'ts' }); ast.program.body[0].specifiers[0].importKind === 'type'; // true

Citations:


Preserve the type modifier when rebuilding import declarations.

buildImportText() reconstructs import statements from specifier names only, ignoring the importKind property. When a declaration like import { type Foo, Bar } from './x' has Bar removed, it rewrites to import { Foo } from './x';, converting a type-only binding to a runtime import and breaking TypeScript semantics or compilation.

Also applies to lines 293-296 where buildImportText() is called.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/devtools-vite/src/remove-devtools.ts` around lines 130 - 173,
buildImportText currently drops TypeScript importKind modifiers, turning
type-only imports into runtime imports; fix by preserving specifier-level and
declaration-level type markers: when rendering named specifiers, prefix any spec
with spec.importKind === 'type' with "type " (e.g. "type Foo" or "type Foo as
Local"), and if defaultSpecifier or namespaceSpecifier have importKind ===
'type' or if every kept specifier is type-only then emit the declaration-level
"import type" (instead of plain "import"); update buildImportText and the places
that call it (where it's used around lines 293-296) so rebuilt import strings
preserve spec.importKind and the declaration-level type marker when appropriate.

Comment on lines +261 to +298
if (finalReferences.length > 0) {
const finalReferenceSet = new Set(finalReferences)

export function removeDevtools(code: string, id: string) {
const [filePath] = id.split('?')
for (const importDecl of importDeclarations) {
if (isTanStackDevtoolsImport(importDecl.source.value)) {
continue
}

const importSpecifiers = importDecl.specifiers
const removableImportSpecifiers = importSpecifiers.filter(
(specifier: any) =>
specifier.type === 'ImportSpecifier' &&
finalReferenceSet.has(specifier.local.name),
)

if (removableImportSpecifiers.length === 0) {
continue
}

const keptSpecifiers = importSpecifiers.filter(
(specifier: any) => !removableImportSpecifiers.includes(specifier),
)

if (keptSpecifiers.length === 0) {
edits.push({
start: importDecl.start,
end: importDecl.end,
text: '',
})
continue
}

edits.push({
start: importDecl.start,
end: importDecl.end,
text: buildImportText(importDecl, keptSpecifiers),
})
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Don't delete non-devtools imports by name alone.

finalReferenceSet only proves that a binding appeared inside the removed devtools subtree. Lines 270-297 then strip matching specifiers from every other import declaration even when that same binding is still used elsewhere in the file, which can leave surviving JSX/identifiers unresolved.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/devtools-vite/src/remove-devtools.ts` around lines 261 - 298,
finalReferenceSet currently causes removal of any ImportSpecifier whose
local.name appears in finalReferences even if that name is still used elsewhere
in the file; update the removal logic in the loop over importDeclarations (the
code using finalReferenceSet, importSpecifiers, removableImportSpecifiers,
keptSpecifiers and buildImportText) to only remove a specifier when you can
prove the binding has no remaining references outside the removed devtools
subtree — e.g. resolve the binding for specifier.local.name in the current
AST/scope (or consult the existing reference map) and check that all its
references are contained in finalReferences before treating it as removable;
otherwise keep the specifier so imports used elsewhere are not deleted.

Comment on lines +311 to +321
export function removeDevtools(code: string, _id: string) {
try {
const ast = parse(code, {
sourceType: 'module',
plugins: ['jsx', 'typescript'],
})
const didTransform = transform(ast)
if (!didTransform) {
const result = transform(code)
if (!result.didTransform) {
return
}
return gen(ast, {
sourceMaps: true,
retainLines: true,
filename: id,
sourceFileName: filePath,
})
} catch (e) {

return {
code: result.code,
map: null,
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's find and examine the remove-devtools.ts file
find . -name "remove-devtools.ts" -type f

Repository: TanStack/devtools

Length of output: 108


🏁 Script executed:

# Check the structure of the devtools-vite package
fd . packages/devtools-vite/src --type f

Repository: TanStack/devtools

Length of output: 845


🏁 Script executed:

# Read the remove-devtools.ts file around lines 311-321
wc -l packages/devtools-vite/src/remove-devtools.ts

Repository: TanStack/devtools

Length of output: 110


🏁 Script executed:

# Read the removeDevtools function around lines 311-321
sed -n '300,325p' packages/devtools-vite/src/remove-devtools.ts | cat -n

Repository: TanStack/devtools

Length of output: 634


🏁 Script executed:

# Read plugin.ts around lines 312-334
wc -l packages/devtools-vite/src/plugin.ts

Repository: TanStack/devtools

Length of output: 101


🏁 Script executed:

# Read the plugin.ts relevant section
sed -n '300,340p' packages/devtools-vite/src/plugin.ts | cat -n

Repository: TanStack/devtools

Length of output: 1741


🏁 Script executed:

# Search for the transform function in remove-devtools.ts to understand what it returns
grep -n "function transform\|const transform" packages/devtools-vite/src/remove-devtools.ts

Repository: TanStack/devtools

Length of output: 102


🏁 Script executed:

# Read the transform function implementation starting from line 188
sed -n '188,230p' packages/devtools-vite/src/remove-devtools.ts | cat -n

Repository: TanStack/devtools

Length of output: 1451


🏁 Script executed:

# Look for applyEdits function
grep -n "function applyEdits\|const applyEdits" packages/devtools-vite/src/remove-devtools.ts

Repository: TanStack/devtools

Length of output: 123


🏁 Script executed:

# Search for any sourcemap-related imports or utilities in the file
grep -i "sourcemap\|source.map\|sourceMap" packages/devtools-vite/src/remove-devtools.ts

Repository: TanStack/devtools

Length of output: 43


🏁 Script executed:

# Check imports at the top of remove-devtools.ts
head -50 packages/devtools-vite/src/remove-devtools.ts | cat -n

Repository: TanStack/devtools

Length of output: 1653


🏁 Script executed:

# Read the applyEdits function
sed -n '119,160p' packages/devtools-vite/src/remove-devtools.ts | cat -n

Repository: TanStack/devtools

Length of output: 1548


🏁 Script executed:

# Search for sourcemap usage in plugin.ts
grep -i "sourcemap\|source.map\|sourceMap" packages/devtools-vite/src/plugin.ts

Repository: TanStack/devtools

Length of output: 43


🏁 Script executed:

# Check if there are any other transform hooks in plugin.ts that might show how to handle sourcemaps
rg "map:" packages/devtools-vite/src/plugin.ts -A 2 -B 2

Repository: TanStack/devtools

Length of output: 43


🏁 Script executed:

# Look for any other vite-related files that might show sourcemap handling patterns
rg "return \{" packages/devtools-vite/src/plugin.ts | grep -i map

Repository: TanStack/devtools

Length of output: 43


🏁 Script executed:

# Check package.json for sourcemap-related dependencies
grep -i "sourcemap\|source-map" packages/devtools-vite/package.json

Repository: TanStack/devtools

Length of output: 43


🏁 Script executed:

# Check test files to understand expected behavior
cat packages/devtools-vite/src/remove-devtools.test.ts | head -100

Repository: TanStack/devtools

Length of output: 2478


🏁 Script executed:

# Search the entire repo for how other Vite plugins handle sourcemaps
rg "map:" packages/ -A 1 -B 1 | head -50

Repository: TanStack/devtools

Length of output: 858


🏁 Script executed:

# Check if there are any JSDoc comments in remove-devtools.ts about the function
sed -n '300,312p' packages/devtools-vite/src/remove-devtools.ts | cat -n

Repository: TanStack/devtools

Length of output: 366


Generate a sourcemap for the code transformation.

This function receives an id parameter but ignores it (marked as _id). Currently, map: null is hard-coded, which breaks sourcemap chaining when this transform is applied. The transform() function already tracks edit positions; these should be converted to proper line/column mappings and returned as a valid SourceMap instead of null to preserve debugging information through the build pipeline.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/devtools-vite/src/remove-devtools.ts` around lines 311 - 321, The
removeDevtools function currently returns map: null and ignores the _id; update
it to generate and return a proper SourceMap using the edit positions provided
by transform() so sourcemap chaining works. Inside removeDevtools, use the
incoming id (rename _id to id) as the original source name, iterate result's
edit ranges/positions (from transform() — e.g., result.edits/positions or
whatever structure tracks start/end offsets) to build line/column mappings and
produce a valid SourceMap object (using a source-map generator or equivalent
helper used elsewhere in the repo), then return that map instead of null along
with result.code; ensure didTransform handling remains the same and the returned
map references the original id as the source.

@SkandarS0 SkandarS0 marked this pull request as draft March 27, 2026 15:39
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/devtools-vite/src/inject-plugin.ts`:
- Around line 290-299: The insertion logic for new imports adds a leading
newline even when there are no existing imports (imports is empty, importAt ===
0), producing a blank first line; update the branch that constructs importText
(used when importExists is false) to conditionally omit the leading "\n" when
importAt is 0 so the import is inserted at the file start without a preceding
blank line—refer to the variables/functions importExists, imports, lastImport,
importAt, importText and the edits.push call to implement this conditional text
construction.
- Around line 274-282: The insertion logic that computes insertAt (using
node.end - 2 when code[node.end - 2] === '/') can leave a stray extra space
before the self-closing "/>", producing `<TanStackDevtools  plugins={[...]}/>`;
update the insertion logic around where pluginsProp is handled (the code that
sets insertAt and calls edits.push with pluginElement) to skip/truncate any
trailing whitespace before the self-closing slash: when detecting a self-closing
tag (code[node.end - 2] === '/'), scan backwards from node.end - 2 to find the
last non-whitespace character and set insertAt to that position + 1 (so you
insert a single space then `plugins={[...]}`), otherwise keep the existing
node.end - 1 behavior; ensure edits.push still inserts a single leading space
before `plugins={[...]}`.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 40086867-56bd-4dba-abc8-aced1f5eeabc

📥 Commits

Reviewing files that changed from the base of the PR and between b486792 and b59b156.

📒 Files selected for processing (1)
  • packages/devtools-vite/src/inject-plugin.ts

Comment on lines +274 to 282
if (!pluginsProp) {
const insertAt =
code[node.end - 2] === '/' ? node.end - 2 : node.end - 1

// Find the last import declaration
let lastImportIndex = -1
ast.program.body.forEach((node, index) => {
if (t.isImportDeclaration(node)) {
lastImportIndex = index
edits.push({
at: insertAt,
text: ` plugins={[${pluginElement}]}`,
})
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Minor formatting issue when JSX has whitespace before self-closing />.

For <TanStackDevtools />, the check code[node.end - 2] === '/' is still true, but inserting plugins={[...]} before / produces <TanStackDevtools plugins={[...]}/> (double space, no space before /> ).

Consider trimming trailing whitespace before the insertion point for self-closing tags:

🛠️ Suggested fix
       if (!pluginsProp) {
-        const insertAt =
-          code[node.end - 2] === '/' ? node.end - 2 : node.end - 1
+        let insertAt = node.end - 1
+        const isSelfClosing = code[node.end - 2] === '/'
+        if (isSelfClosing) {
+          insertAt = node.end - 2
+          // Trim trailing whitespace before />
+          while (insertAt > 0 && code[insertAt - 1] === ' ') {
+            insertAt--
+          }
+        }
 
         edits.push({
           at: insertAt,
-          text: ` plugins={[${pluginElement}]}`,
+          text: ` plugins={[${pluginElement}]}${isSelfClosing ? ' ' : ''}`,
         })
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!pluginsProp) {
const insertAt =
code[node.end - 2] === '/' ? node.end - 2 : node.end - 1
// Find the last import declaration
let lastImportIndex = -1
ast.program.body.forEach((node, index) => {
if (t.isImportDeclaration(node)) {
lastImportIndex = index
edits.push({
at: insertAt,
text: ` plugins={[${pluginElement}]}`,
})
}
if (!pluginsProp) {
let insertAt = node.end - 1
const isSelfClosing = code[node.end - 2] === '/'
if (isSelfClosing) {
insertAt = node.end - 2
// Trim trailing whitespace before />
while (insertAt > 0 && code[insertAt - 1] === ' ') {
insertAt--
}
}
edits.push({
at: insertAt,
text: ` plugins={[${pluginElement}]}${isSelfClosing ? ' ' : ''}`,
})
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/devtools-vite/src/inject-plugin.ts` around lines 274 - 282, The
insertion logic that computes insertAt (using node.end - 2 when code[node.end -
2] === '/') can leave a stray extra space before the self-closing "/>",
producing `<TanStackDevtools  plugins={[...]}/>`; update the insertion logic
around where pluginsProp is handled (the code that sets insertAt and calls
edits.push with pluginElement) to skip/truncate any trailing whitespace before
the self-closing slash: when detecting a self-closing tag (code[node.end - 2]
=== '/'), scan backwards from node.end - 2 to find the last non-whitespace
character and set insertAt to that position + 1 (so you insert a single space
then `plugins={[...]}`), otherwise keep the existing node.end - 1 behavior;
ensure edits.push still inserts a single leading space before `plugins={[...]}`.

Comment on lines +290 to +299
if (!importExists) {
const imports = ast.program.body.filter(
(node) => node.type === 'ImportDeclaration',
)
const lastImport = imports[imports.length - 1]
const importAt = lastImport ? lastImport.end : 0

/**
* Detects if a file contains TanStack devtools import
*/
export function detectDevtoolsFile(code: string): boolean {
return detectDevtoolsImport(code)
const importText = `\nimport { ${importName} } from ${JSON.stringify(injection.packageName)};`
edits.push({ at: importAt, text: importText })
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Minor: Leading blank line when file has no existing imports.

When imports is empty, importAt becomes 0, and inserting \nimport {...} at position 0 creates a blank line at the start of the file.

🛠️ Suggested fix
     const imports = ast.program.body.filter(
       (node) => node.type === 'ImportDeclaration',
     )
     const lastImport = imports[imports.length - 1]
     const importAt = lastImport ? lastImport.end : 0
 
-    const importText = `\nimport { ${importName} } from ${JSON.stringify(injection.packageName)};`
+    const importText = lastImport
+      ? `\nimport { ${importName} } from ${JSON.stringify(injection.packageName)};`
+      : `import { ${importName} } from ${JSON.stringify(injection.packageName)};\n`
     edits.push({ at: importAt, text: importText })
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!importExists) {
const imports = ast.program.body.filter(
(node) => node.type === 'ImportDeclaration',
)
const lastImport = imports[imports.length - 1]
const importAt = lastImport ? lastImport.end : 0
/**
* Detects if a file contains TanStack devtools import
*/
export function detectDevtoolsFile(code: string): boolean {
return detectDevtoolsImport(code)
const importText = `\nimport { ${importName} } from ${JSON.stringify(injection.packageName)};`
edits.push({ at: importAt, text: importText })
}
if (!importExists) {
const imports = ast.program.body.filter(
(node) => node.type === 'ImportDeclaration',
)
const lastImport = imports[imports.length - 1]
const importAt = lastImport ? lastImport.end : 0
const importText = lastImport
? `\nimport { ${importName} } from ${JSON.stringify(injection.packageName)};`
: `import { ${importName} } from ${JSON.stringify(injection.packageName)};\n`
edits.push({ at: importAt, text: importText })
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/devtools-vite/src/inject-plugin.ts` around lines 290 - 299, The
insertion logic for new imports adds a leading newline even when there are no
existing imports (imports is empty, importAt === 0), producing a blank first
line; update the branch that constructs importText (used when importExists is
false) to conditionally omit the leading "\n" when importAt is 0 so the import
is inserted at the file start without a preceding blank line—refer to the
variables/functions importExists, imports, lastImport, importAt, importText and
the edits.push call to implement this conditional text construction.

@SkandarS0
Copy link
Copy Markdown
Author

SkandarS0 commented Mar 27, 2026

Hi, @AlemTuzlak
The oxc team for now recommends using ersrap as this issue and their docs, as they did not implement the Node API bindings for their oxc_codegen crate which is a feature request as this closed discussion for generating code from ASTs.

Though the rolldown team is working on adding support for magic-string as this repository, this issue and their docs

So, for you to decide if we either have to use magic-string or esrap.

Thanks.

@AlemTuzlak
Copy link
Copy Markdown
Collaborator

Whats more performant of thw two? I personally like the former as the dx looks much friendlier than magic string?

@SkandarS0
Copy link
Copy Markdown
Author

I will look into their performance differences, and I'll come back

@SkandarS0
Copy link
Copy Markdown
Author

I think if we want full performance, we will need to use both, in cases where we don't manipulate the AST nodes structure, like the enhance logging feature, it's more efficient to use magic-string it's just replacing a statement with another, but when dealing with AST nodes, like the remove devtools feature, which manipulates the AST nodes, it's better to use esrap.

@AlemTuzlak
Copy link
Copy Markdown
Collaborator

Lets go eith esrap then, I highly prefer it and dont want 2 solutions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants