perf(vite-plugin): replace babel with oxc#407
perf(vite-plugin): replace babel with oxc#407SkandarS0 wants to merge 7 commits intoTanStack:mainfrom
Conversation
oxc parser doesn't add or remove semi-colons, commas...
📝 WalkthroughWalkthroughReplaced 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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
| "devtools" | ||
| ], | ||
| "type": "module", | ||
| "types": "dist/esm//index.d.ts", |
There was a problem hiding this comment.
I have also fixed this // that was there for like a decade
AlemTuzlak
left a comment
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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) => |
There was a problem hiding this comment.
Can we make these not be any, in all thw changed files i dont want casts like these
There was a problem hiding this comment.
oww you're right I forgot to remove those
There was a problem hiding this comment.
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 | 🟡 MinorFix the
console.errorno-call fixture.The test name says this covers a bare
console.error, but Line 109 now usesconsole.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. WhilefilePathwill typically be defined (sinceid.split('?')always returns at least one element), the assertion could mask issues ifidis 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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (18)
_artifacts/domain_map.yamldocs/architecture.mddocs/source-inspector.mddocs/vite-plugin.mdpackages/devtools-vite/package.jsonpackages/devtools-vite/skills/devtools-vite-plugin/SKILL.mdpackages/devtools-vite/skills/devtools-vite-plugin/references/vite-options.mdpackages/devtools-vite/src/babel.tspackages/devtools-vite/src/enhance-logs.test.tspackages/devtools-vite/src/enhance-logs.tspackages/devtools-vite/src/inject-plugin.test.tspackages/devtools-vite/src/inject-plugin.tspackages/devtools-vite/src/inject-source.test.tspackages/devtools-vite/src/inject-source.tspackages/devtools-vite/src/remove-devtools.test.tspackages/devtools-vite/src/remove-devtools.tspackages/devtools/skills/devtools-marketplace/SKILL.mdpackages/devtools/skills/devtools-production/SKILL.md
💤 Files with no reviewable changes (1)
- packages/devtools-vite/src/babel.ts
| 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 → ` |
There was a problem hiding this comment.
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.
| 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}';` |
There was a problem hiding this comment.
❓ Verification inconclusive
Script executed:
cat -n packages/devtools-vite/src/remove-devtools.ts | head -200Repository: 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 -iRepository: 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.tsRepository: 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 2Repository: 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 -20Repository: 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 tsRepository: 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 ASTRepository: 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:
- 1: Improve AST with respect to estree oxc-project/oxc#2854
- 2: https://www.npmjs.com/package/oxc-parser
- 3: https://docs.rs/oxc_ast/latest/oxc_ast/ast/struct.ImportSpecifier.html
- 4: fix(parser): string
ImportSpecifiers for type imports oxc-project/oxc#6352 - 5: feat(module_lexer): distinguish for types-only imports and exports oxc-project/oxc#5184
- 6: feat(parser): report error when
import type { type }is used oxc-project/oxc#10528 - 7: ESTree AST and Oxc AST type discrepancies oxc-project/oxc#10139
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.
| 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), | ||
| }) | ||
| } |
There was a problem hiding this comment.
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.
| 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, | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's find and examine the remove-devtools.ts file
find . -name "remove-devtools.ts" -type fRepository: TanStack/devtools
Length of output: 108
🏁 Script executed:
# Check the structure of the devtools-vite package
fd . packages/devtools-vite/src --type fRepository: 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.tsRepository: 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 -nRepository: TanStack/devtools
Length of output: 634
🏁 Script executed:
# Read plugin.ts around lines 312-334
wc -l packages/devtools-vite/src/plugin.tsRepository: 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 -nRepository: 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.tsRepository: 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 -nRepository: TanStack/devtools
Length of output: 1451
🏁 Script executed:
# Look for applyEdits function
grep -n "function applyEdits\|const applyEdits" packages/devtools-vite/src/remove-devtools.tsRepository: 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.tsRepository: 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 -nRepository: TanStack/devtools
Length of output: 1653
🏁 Script executed:
# Read the applyEdits function
sed -n '119,160p' packages/devtools-vite/src/remove-devtools.ts | cat -nRepository: 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.tsRepository: 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 2Repository: 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 mapRepository: TanStack/devtools
Length of output: 43
🏁 Script executed:
# Check package.json for sourcemap-related dependencies
grep -i "sourcemap\|source-map" packages/devtools-vite/package.jsonRepository: 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 -100Repository: 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 -50Repository: 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 -nRepository: 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.
There was a problem hiding this comment.
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
📒 Files selected for processing (1)
packages/devtools-vite/src/inject-plugin.ts
| 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}]}`, | ||
| }) | ||
| } |
There was a problem hiding this comment.
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.
| 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={[...]}`.
| 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 }) | ||
| } |
There was a problem hiding this comment.
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.
| 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.
|
Hi, @AlemTuzlak Though the rolldown team is working on adding support for So, for you to decide if we either have to use magic-string or esrap. Thanks. |
|
Whats more performant of thw two? I personally like the former as the dx looks much friendlier than magic string? |
|
I will look into their performance differences, and I'll come back |
|
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 |
|
Lets go eith esrap then, I highly prefer it and dont want 2 solutions |
🎯 Changes
As the title says, this PR replaces babel transformer with the oxc-parser package and its visitor:
✅ Checklist
pnpm test:pr.🚀 Release Impact
Summary by CodeRabbit
Release Notes