From 537c73d74f2174538d5783f47454073c06bb9c70 Mon Sep 17 00:00:00 2001 From: Skandar Souissi Date: Mon, 16 Mar 2026 20:55:41 +0100 Subject: [PATCH 1/6] perf(vite-plugin): replace babel with oxc --- packages/devtools-vite/package.json | 11 +- packages/devtools-vite/src/babel.ts | 18 - .../devtools-vite/src/enhance-logs.test.ts | 4 +- packages/devtools-vite/src/enhance-logs.ts | 208 +++-- .../devtools-vite/src/inject-plugin.test.ts | 751 ++++++------------ packages/devtools-vite/src/inject-plugin.ts | 501 ++++++------ .../devtools-vite/src/inject-source.test.ts | 126 +-- packages/devtools-vite/src/inject-source.ts | 389 ++++----- .../devtools-vite/src/remove-devtools.test.ts | 159 ++-- packages/devtools-vite/src/remove-devtools.ts | 447 +++++++---- pnpm-lock.yaml | 242 +++++- 11 files changed, 1441 insertions(+), 1415 deletions(-) delete mode 100644 packages/devtools-vite/src/babel.ts diff --git a/packages/devtools-vite/package.json b/packages/devtools-vite/package.json index 83f4a6f6..c696149e 100644 --- a/packages/devtools-vite/package.json +++ b/packages/devtools-vite/package.json @@ -18,7 +18,7 @@ "devtools" ], "type": "module", - "types": "dist/esm//index.d.ts", + "types": "dist/esm/index.d.ts", "module": "dist/esm/index.js", "exports": { ".": { @@ -56,21 +56,14 @@ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "dependencies": { - "@babel/core": "^7.28.4", - "@babel/generator": "^7.28.3", - "@babel/parser": "^7.28.4", - "@babel/traverse": "^7.28.4", - "@babel/types": "^7.28.4", "@tanstack/devtools-client": "workspace:*", "@tanstack/devtools-event-bus": "workspace:*", "chalk": "^5.6.2", "launch-editor": "^2.11.1", + "oxc-parser": "0.120.0", "picomatch": "^4.0.3" }, "devDependencies": { - "@types/babel__core": "^7.20.5", - "@types/babel__generator": "^7.27.0", - "@types/babel__traverse": "^7.28.0", "@types/picomatch": "^4.0.2", "happy-dom": "^18.0.1" } diff --git a/packages/devtools-vite/src/babel.ts b/packages/devtools-vite/src/babel.ts deleted file mode 100644 index 7a17645f..00000000 --- a/packages/devtools-vite/src/babel.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { parse } from '@babel/parser' -import * as t from '@babel/types' -import generate from '@babel/generator' -import traverse from '@babel/traverse' - -export { parse, t } - -export const trav = - typeof (traverse as any).default !== 'undefined' - ? // eslint-disable-next-line @typescript-eslint/consistent-type-imports - ((traverse as any).default as typeof import('@babel/traverse').default) - : traverse - -export const gen = - typeof (generate as any).default !== 'undefined' - ? // eslint-disable-next-line @typescript-eslint/consistent-type-imports - ((generate as any).default as typeof import('@babel/generator').default) - : generate diff --git a/packages/devtools-vite/src/enhance-logs.test.ts b/packages/devtools-vite/src/enhance-logs.test.ts index 995e4dd6..6d23f860 100644 --- a/packages/devtools-vite/src/enhance-logs.test.ts +++ b/packages/devtools-vite/src/enhance-logs.test.ts @@ -28,7 +28,7 @@ describe('enhance-logs', () => { test('it does not add enhanced console.logs to console.log that is not called', () => { const output = enhanceConsoleLog( ` - console.log + console.log `, 'test.jsx', 3000, @@ -106,7 +106,7 @@ describe('enhance-logs', () => { test('it does not add enhanced console.error to console.error that is not called', () => { const output = enhanceConsoleLog( ` - console.log + console.log `, 'test.jsx', 3000, diff --git a/packages/devtools-vite/src/enhance-logs.ts b/packages/devtools-vite/src/enhance-logs.ts index 8c6d3670..502290c1 100644 --- a/packages/devtools-vite/src/enhance-logs.ts +++ b/packages/devtools-vite/src/enhance-logs.ts @@ -1,78 +1,73 @@ import chalk from 'chalk' import { normalizePath } from 'vite' -import { gen, parse, t, trav } from './babel' -import type { types as Babel } from '@babel/core' -import type { ParseResult } from '@babel/parser' - -const transform = ( - ast: ParseResult, - filePath: string, - port: number, -) => { - let didTransform = false - - trav(ast, { - CallExpression(path) { - const callee = path.node.callee - // Match console.log(...) or console.error(...) - if ( - callee.type === 'MemberExpression' && - callee.object.type === 'Identifier' && - callee.object.name === 'console' && - callee.property.type === 'Identifier' && - (callee.property.name === 'log' || callee.property.name === 'error') - ) { - const location = path.node.loc - if (!location) { - return - } - const [lineNumber, column] = [ - location.start.line, - location.start.column, - ] - const finalPath = `${filePath}:${lineNumber}:${column + 1}` - const logMessage = `${chalk.magenta('LOG')} ${chalk.blueBright(`${finalPath}`)}\n → ` - - const serverLogMessage = t.arrayExpression([ - t.stringLiteral(logMessage), - ]) - const browserLogMessage = t.arrayExpression([ - // LOG with css formatting specifiers: %c - t.stringLiteral( - `%c${'LOG'}%c %c${`Go to Source: http://localhost:${port}/__tsd/open-source?source=${encodeURIComponent(finalPath)}`}%c \n → `, - ), - // magenta - t.stringLiteral('color:#A0A'), - t.stringLiteral('color:#FFF'), - // blueBright - t.stringLiteral('color:#55F'), - t.stringLiteral('color:#FFF'), - ]) - - // typeof window === "undefined" - const checkServerCondition = t.binaryExpression( - '===', - t.unaryExpression('typeof', t.identifier('window')), - t.stringLiteral('undefined'), - ) - - // ...(isServer ? serverLogMessage : browserLogMessage) - path.node.arguments.unshift( - t.spreadElement( - t.conditionalExpression( - checkServerCondition, - serverLogMessage, - browserLogMessage, - ), - ), - ) - - didTransform = true - } - }, - }) - - return didTransform +import { Visitor, parseSync } from 'oxc-parser' +import type { CallExpression, MemberExpression } from 'oxc-parser' + +type Insertion = { + at: number + text: string +} + +const buildLineStarts = (source: string) => { + const starts = [0] + for (let i = 0; i < source.length; i++) { + if (source[i] === '\n') { + starts.push(i + 1) + } + } + return starts +} + +const offsetToLineColumn = (offset: number, lineStarts: Array) => { + // Binary search to find the nearest line start <= offset. + let low = 0 + let high = lineStarts.length - 1 + + while (low <= high) { + const mid = (low + high) >> 1 + const lineStart = lineStarts[mid] + if (lineStart === undefined) { + break + } + + if (lineStart <= offset) { + low = mid + 1 + } else { + high = mid - 1 + } + } + + const lineIndex = Math.max(0, high) + const lineStart = lineStarts[lineIndex] ?? 0 + + return { + line: lineIndex + 1, + column: offset - lineStart + 1, + } +} + +const isConsoleMemberExpression = ( + callee: CallExpression['callee'], +): callee is MemberExpression => { + return ( + callee.type === 'MemberExpression' && + callee.computed === false && + callee.object.type === 'Identifier' && + callee.object.name === 'console' && + callee.property.type === 'Identifier' && + (callee.property.name === 'log' || callee.property.name === 'error') + ) +} + +const applyInsertions = (source: string, insertions: Array) => { + const ordered = [...insertions].sort((a, b) => b.at - a.at) + + let next = source + for (const insertion of ordered) { + next = next.slice(0, insertion.at) + insertion.text + next.slice(insertion.at) + } + + return next } export function enhanceConsoleLog(code: string, id: string, port: number) { @@ -81,21 +76,66 @@ export function enhanceConsoleLog(code: string, id: string, port: number) { const location = filePath?.replace(normalizePath(process.cwd()), '')! try { - const ast = parse(code, { + const result = parseSync(filePath ?? id, code, { sourceType: 'module', - plugins: ['jsx', 'typescript'], + lang: 'tsx', + range: true, }) - const didTransform = transform(ast, location, port) - if (!didTransform) { + + if (result.errors.length > 0) { return } - return gen(ast, { - sourceMaps: true, - retainLines: true, - filename: id, - sourceFileName: filePath, - }) - } catch (e) { + + const insertions: Array = [] + const lineStarts = buildLineStarts(code) + + new Visitor({ + CallExpression(node) { + if (!isConsoleMemberExpression(node.callee)) { + return + } + + const { line, column } = offsetToLineColumn(node.start, lineStarts) + const finalPath = `${location}:${line}:${column}` + + 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 → ` + + const argsArray = + `[${JSON.stringify(serverLogMessage)}]` + + ` : [${JSON.stringify(browserLogMessage)},` + + `${JSON.stringify('color:#A0A')},` + + `${JSON.stringify('color:#FFF')},` + + `${JSON.stringify('color:#55F')},` + + `${JSON.stringify('color:#FFF')}]` + + const injectedPrefix = + `...(typeof window === 'undefined' ? ${argsArray})` + + `${node.arguments.length > 0 ? ', ' : ''}` + + const insertionPoint = + node.arguments[0]?.start !== undefined + ? node.arguments[0].start + : node.end - 1 + + insertions.push({ + at: insertionPoint, + text: injectedPrefix, + }) + }, + }).visit(result.program) + + if (insertions.length === 0) { + return + } + + return { + code: applyInsertions(code, insertions), + map: null, + } + } catch { return } } diff --git a/packages/devtools-vite/src/inject-plugin.test.ts b/packages/devtools-vite/src/inject-plugin.test.ts index 933b98d4..82ff7fe5 100644 --- a/packages/devtools-vite/src/inject-plugin.test.ts +++ b/packages/devtools-vite/src/inject-plugin.test.ts @@ -4,13 +4,18 @@ import { findDevtoolsComponentName, transformAndInject, } from './inject-plugin' -import { gen, parse } from './babel' const removeEmptySpace = (str: string) => { return str.replace(/\s/g, '').trim() } -// Helper to test transformation without file I/O +const expectContains = (code: string, snippets: Array) => { + const normalized = removeEmptySpace(code) + for (const snippet of snippets) { + expect(normalized.includes(removeEmptySpace(snippet))).toBe(true) + } +} + const testTransform = ( code: string, packageName: string, @@ -20,27 +25,21 @@ const testTransform = ( type: 'jsx' | 'function' }, ) => { - const ast = parse(code, { - sourceType: 'module', - plugins: ['jsx', 'typescript'], - }) - - const devtoolsComponentName = findDevtoolsComponentName(ast) + const devtoolsComponentName = findDevtoolsComponentName(code) if (!devtoolsComponentName) { return { transformed: false, code } } - const didTransform = transformAndInject( - ast, + const result = transformAndInject( + code, { packageName, pluginName, pluginImport }, devtoolsComponentName, ) - if (!didTransform) { + if (!result.didTransform) { return { transformed: false, code } } - const result = gen(ast, { sourceMaps: false, retainLines: false }) return { transformed: true, code: result.code } } @@ -103,7 +102,7 @@ describe('inject-plugin', () => { test('should add plugin to existing empty plugins array', () => { const code = ` import { TanStackDevtools } from '@tanstack/react-devtools' - + function App() { return } @@ -120,26 +119,17 @@ describe('inject-plugin', () => { ) expect(result.transformed).toBe(true) - expect(removeEmptySpace(result.code)).toBe( - removeEmptySpace(` - import { TanStackDevtools } from '@tanstack/react-devtools'; - import { ReactQueryDevtoolsPanel } from "@tanstack/react-query-devtools"; - - function App() { - return - }]} />; - } - `), - ) + expectContains(result.code, [ + 'import { ReactQueryDevtoolsPanel } from "@tanstack/react-query-devtools"', + 'plugins={[{ name: "TanStack Query", render: }]}', + ]) }) test('should add plugin to existing plugins array with other plugins', () => { const code = ` import { TanStackDevtools } from '@tanstack/react-devtools' import { OtherPlugin } from '@tanstack/other-plugin' - + function App() { return } @@ -158,29 +148,16 @@ describe('inject-plugin', () => { ) expect(result.transformed).toBe(true) - expect(removeEmptySpace(result.code)).toBe( - removeEmptySpace(` - import { TanStackDevtools } from '@tanstack/react-devtools'; - import { OtherPlugin } from '@tanstack/other-plugin'; - import { ReactQueryDevtoolsPanel } from "@tanstack/react-query-devtools"; - - function App() { - return }, - { - name: "TanStack Query", - render: - } - ]} />; - } - `), - ) + expectContains(result.code, [ + '{ name: \'other\', render: }', + '{ name: "TanStack Query", render: }', + ]) }) test('should create plugins prop if it does not exist', () => { const code = ` import { TanStackDevtools } from '@tanstack/react-devtools' - + function App() { return } @@ -197,25 +174,16 @@ describe('inject-plugin', () => { ) expect(result.transformed).toBe(true) - expect(removeEmptySpace(result.code)).toBe( - removeEmptySpace(` - import { TanStackDevtools } from '@tanstack/react-devtools'; - import { ReactQueryDevtoolsPanel } from "@tanstack/react-query-devtools"; - - function App() { - return - }]} />; - } - `), - ) + expectContains(result.code, [ + 'import { ReactQueryDevtoolsPanel } from "@tanstack/react-query-devtools"', + 'plugins={[{ name: "TanStack Query", render: }]}', + ]) }) test('should create plugins prop with other existing props', () => { const code = ` import { TanStackDevtools } from '@tanstack/react-devtools' - + function App() { return } @@ -232,26 +200,16 @@ describe('inject-plugin', () => { ) expect(result.transformed).toBe(true) - expect(removeEmptySpace(result.code)).toBe( - removeEmptySpace(` - import { TanStackDevtools } from '@tanstack/react-devtools'; - import { ReactQueryDevtoolsPanel } from "@tanstack/react-query-devtools"; - - function App() { - return - }]} />; - } - `), - ) + expectContains(result.code, [ + ' }]}', + ]) }) test('should not add plugin if it already exists', () => { const code = ` import { TanStackDevtools } from '@tanstack/react-devtools' import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools' - + function App() { return } @@ -277,7 +235,7 @@ describe('inject-plugin', () => { test('should handle renamed named import', () => { const code = ` import { TanStackDevtools as DevtoolsPanel } from '@tanstack/react-devtools' - + function App() { return } @@ -294,25 +252,15 @@ describe('inject-plugin', () => { ) expect(result.transformed).toBe(true) - expect(removeEmptySpace(result.code)).toBe( - removeEmptySpace(` - import { TanStackDevtools as DevtoolsPanel } from '@tanstack/react-devtools'; - import { ReactQueryDevtoolsPanel } from "@tanstack/react-query-devtools"; - - function App() { - return - }]} />; - } - `), - ) + expectContains(result.code, [ + ' }]}', + ]) }) test('should handle renamed import without plugins prop', () => { const code = ` import { TanStackDevtools as MyDevtools } from '@tanstack/solid-devtools' - + function App() { return } @@ -329,19 +277,9 @@ describe('inject-plugin', () => { ) expect(result.transformed).toBe(true) - expect(removeEmptySpace(result.code)).toBe( - removeEmptySpace(` - import { TanStackDevtools as MyDevtools } from '@tanstack/solid-devtools'; - import { ReactQueryDevtoolsPanel } from "@tanstack/react-query-devtools"; - - function App() { - return - }]} />; - } - `), - ) + expectContains(result.code, [ + ' }]}', + ]) }) }) @@ -349,7 +287,7 @@ describe('inject-plugin', () => { test('should handle namespace import', () => { const code = ` import * as DevtoolsModule from '@tanstack/react-devtools' - + function App() { return } @@ -366,25 +304,15 @@ describe('inject-plugin', () => { ) expect(result.transformed).toBe(true) - expect(removeEmptySpace(result.code)).toBe( - removeEmptySpace(` - import * as DevtoolsModule from '@tanstack/react-devtools'; - import { ReactQueryDevtoolsPanel } from "@tanstack/react-query-devtools"; - - function App() { - return - }]} />; - } - `), - ) + expectContains(result.code, [ + ' }]}', + ]) }) test('should handle namespace import without plugins prop', () => { const code = ` import * as TSD from '@tanstack/solid-devtools' - + function App() { return } @@ -401,19 +329,9 @@ describe('inject-plugin', () => { ) expect(result.transformed).toBe(true) - expect(removeEmptySpace(result.code)).toBe( - removeEmptySpace(` - import * as TSD from '@tanstack/solid-devtools'; - import { SolidRouterDevtoolsPanel } from "@tanstack/solid-router-devtools"; - - function App() { - return - }]} />; - } - `), - ) + expectContains(result.code, [ + ' }]}', + ]) }) }) @@ -421,7 +339,7 @@ describe('inject-plugin', () => { test('should handle router devtools', () => { const code = ` import { TanStackDevtools } from '@tanstack/react-devtools' - + function App() { return } @@ -438,25 +356,15 @@ describe('inject-plugin', () => { ) expect(result.transformed).toBe(true) - expect(removeEmptySpace(result.code)).toBe( - removeEmptySpace(` - import { TanStackDevtools } from '@tanstack/react-devtools'; - import { ReactRouterDevtoolsPanel } from "@tanstack/react-router-devtools"; - - function App() { - return - }]} />; - } - `), - ) + expectContains(result.code, [ + '{ name: "TanStack Router", render: }', + ]) }) test('should handle form devtools', () => { const code = ` import { TanStackDevtools } from '@tanstack/solid-devtools' - + function App() { return } @@ -473,25 +381,15 @@ describe('inject-plugin', () => { ) expect(result.transformed).toBe(true) - expect(removeEmptySpace(result.code)).toBe( - removeEmptySpace(` - import { TanStackDevtools } from '@tanstack/solid-devtools'; - import { ReactFormDevtoolsPanel } from "@tanstack/react-form-devtools"; - - function App() { - return - }]} />; - } - `), - ) + expectContains(result.code, [ + '{ name: "TanStack Form", render: }', + ]) }) test('should handle query devtools', () => { const code = ` import { TanStackDevtools } from '@tanstack/vue-devtools' - + function App() { return } @@ -508,316 +406,193 @@ describe('inject-plugin', () => { ) expect(result.transformed).toBe(true) - expect(removeEmptySpace(result.code)).toBe( - removeEmptySpace(` - import { TanStackDevtools } from '@tanstack/vue-devtools'; - import { ReactQueryDevtoolsPanel } from "@tanstack/react-query-devtools"; - - function App() { - return - }]} />; - } - `), - ) + expectContains(result.code, [ + '{ name: "TanStack Query", render: }', + ]) }) }) - describe('edge cases', () => { - test('should not transform files without TanStackDevtools component', () => { + describe('function-based plugins', () => { + test('should add function plugin to empty plugins array', () => { const code = ` import { TanStackDevtools } from '@tanstack/react-devtools' - + function App() { - return
Hello World
+ return } ` const result = testTransform( code, - '@tanstack/react-query-devtools', - 'TanStack Query', + '@tanstack/react-form-devtools', + 'react-form', { - importName: 'ReactQueryDevtoolsPanel', - type: 'jsx', + importName: 'FormDevtoolsPlugin', + type: 'function', }, ) - expect(result.transformed).toBe(false) + expect(result.transformed).toBe(true) + expectContains(result.code, ['plugins={[FormDevtoolsPlugin()]}']) }) - test('should handle TanStackDevtools with children', () => { + test('should add function plugin alongside JSX plugins', () => { const code = ` import { TanStackDevtools } from '@tanstack/react-devtools' - + import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools' + function App() { - return ( - -
Custom content
-
- ) + return } + ]} /> } ` const result = testTransform( code, - '@tanstack/react-query-devtools', - 'TanStack Query', + '@tanstack/react-form-devtools', + 'react-form', { - importName: 'ReactQueryDevtoolsPanel', - type: 'jsx', + importName: 'FormDevtoolsPlugin', + type: 'function', }, ) expect(result.transformed).toBe(true) - expect(removeEmptySpace(result.code)).toBe( - removeEmptySpace(` - import { TanStackDevtools } from '@tanstack/react-devtools'; - import { ReactQueryDevtoolsPanel } from "@tanstack/react-query-devtools"; - - function App() { - return - }]}> -
Custom content
-
; - } - `), - ) + expectContains(result.code, [ + '{ name: \'TanStack Query\', render: }', + 'FormDevtoolsPlugin()', + ]) }) - test('should handle multiple TanStackDevtools in same file', () => { + test('should create plugins prop with function plugin when it does not exist', () => { const code = ` import { TanStackDevtools } from '@tanstack/react-devtools' - + function App() { - return ( - <> - - - - ) + return } ` const result = testTransform( code, - '@tanstack/react-query-devtools', - 'TanStack Query', + '@tanstack/react-form-devtools', + 'react-form', { - importName: 'ReactQueryDevtoolsPanel', - type: 'jsx', + importName: 'FormDevtoolsPlugin', + type: 'function', }, ) expect(result.transformed).toBe(true) - expect(removeEmptySpace(result.code)).toBe( - removeEmptySpace(` - import { TanStackDevtools } from '@tanstack/react-devtools'; - import { ReactQueryDevtoolsPanel } from "@tanstack/react-query-devtools"; - - function App() { - return <> - - }]} /> - - }]} /> - ; - } - `), - ) + expectContains(result.code, ['plugins={[FormDevtoolsPlugin()]}']) }) - test('should handle TanStackDevtools deeply nested', () => { + test('should not add function plugin if it already exists', () => { const code = ` import { TanStackDevtools } from '@tanstack/react-devtools' - + import { FormDevtoolsPlugin } from '@tanstack/react-form-devtools' + function App() { - return ( -
-
- -
-
- ) + return } ` const result = testTransform( code, - '@tanstack/react-query-devtools', - 'TanStack Query', + '@tanstack/react-form-devtools', + 'react-form', { - importName: 'ReactQueryDevtoolsPanel', - type: 'jsx', + importName: 'FormDevtoolsPlugin', + type: 'function', }, ) - expect(result.transformed).toBe(true) - expect(removeEmptySpace(result.code)).toBe( - removeEmptySpace(` - import { TanStackDevtools } from '@tanstack/react-devtools'; - import { ReactQueryDevtoolsPanel } from "@tanstack/react-query-devtools"; - - function App() { - return
-
- -
-
; - } - `), - ) + expect(result.transformed).toBe(false) }) - test('should preserve existing code formatting and structure', () => { + test('should handle function plugin with renamed devtools import', () => { const code = ` - import { TanStackDevtools } from '@tanstack/react-devtools' - import { useState } from 'react' - + import { TanStackDevtools as DevtoolsPanel } from '@tanstack/react-devtools' + function App() { - const [count, setCount] = useState(0) - - return ( -
- - -
- ) + return } ` const result = testTransform( code, - '@tanstack/react-query-devtools', - 'TanStack Query', + '@tanstack/react-form-devtools', + 'react-form', { - importName: 'ReactQueryDevtoolsPanel', - type: 'jsx', + importName: 'FormDevtoolsPlugin', + type: 'function', }, ) expect(result.transformed).toBe(true) - expect(removeEmptySpace(result.code)).toBe( - removeEmptySpace(` - import { TanStackDevtools } from '@tanstack/react-devtools'; - import { useState } from 'react'; - import { ReactQueryDevtoolsPanel } from "@tanstack/react-query-devtools"; - - function App() { - const [count, setCount] = useState(0); - return
- - - }]} /> -
; - } - `), - ) + expectContains(result.code, [' { + test('should handle function plugin with namespace import', () => { const code = ` - import { TanStackDevtools } from '@tanstack/react-devtools' - import type { FC } from 'react' - - const App: FC = () => { - return + import * as DevtoolsModule from '@tanstack/solid-devtools' + + function App() { + return } ` const result = testTransform( code, - '@tanstack/react-query-devtools', - 'TanStack Query', + '@tanstack/solid-form-devtools', + 'solid-form', { - importName: 'ReactQueryDevtoolsPanel', - type: 'jsx', + importName: 'FormDevtoolsPlugin', + type: 'function', }, ) expect(result.transformed).toBe(true) - expect(removeEmptySpace(result.code)).toBe( - removeEmptySpace(` - import { TanStackDevtools } from '@tanstack/react-devtools'; - import type { FC } from 'react'; - import { ReactQueryDevtoolsPanel } from "@tanstack/react-query-devtools"; - - const App: FC = () => { - return - }]} />; - }; - `), - ) + expectContains(result.code, [ + ' { + test('should add multiple function plugins correctly', () => { const code = ` import { TanStackDevtools } from '@tanstack/react-devtools' - + import { FormDevtoolsPlugin } from '@tanstack/react-form-devtools' + function App() { - return }, - ]} /> + return } ` const result = testTransform( code, - '@tanstack/react-query-devtools', - 'TanStack Query', + '@tanstack/react-router-devtools', + 'react-router', { - importName: 'ReactQueryDevtoolsPanel', - type: 'jsx', + importName: 'RouterDevtoolsPlugin', + type: 'function', }, ) expect(result.transformed).toBe(true) - expect(removeEmptySpace(result.code)).toBe( - removeEmptySpace(` - import { TanStackDevtools } from '@tanstack/react-devtools'; - import { ReactQueryDevtoolsPanel } from "@tanstack/react-query-devtools"; - - function App() { - return }, - { - name: "TanStack Query", - render: - } - ]} />; - } - `), - ) + expectContains(result.code, [ + 'plugins={[FormDevtoolsPlugin(), RouterDevtoolsPlugin()]}', + ]) }) + }) - test('should not transform if devtools import not found', () => { + describe('edge cases', () => { + test('should not transform files without TanStackDevtools component', () => { const code = ` - import { SomeOtherComponent } from 'some-package' - + import { TanStackDevtools } from '@tanstack/react-devtools' + function App() { - return + return
Hello World
} ` @@ -833,13 +608,11 @@ describe('inject-plugin', () => { expect(result.transformed).toBe(false) }) - }) - describe('function-based plugins', () => { - test('should add function plugin to empty plugins array', () => { + test('should not transform when pluginImport is not provided', () => { const code = ` import { TanStackDevtools } from '@tanstack/react-devtools' - + function App() { return } @@ -847,235 +620,225 @@ describe('inject-plugin', () => { const result = testTransform( code, - '@tanstack/react-form-devtools', - 'react-form', + '@tanstack/react-query-devtools', + 'TanStack Query', { - importName: 'FormDevtoolsPlugin', - type: 'function', + importName: '', + type: 'jsx', }, ) - expect(result.transformed).toBe(true) - expect(removeEmptySpace(result.code)).toBe( - removeEmptySpace(` - import { TanStackDevtools } from '@tanstack/react-devtools'; - import { FormDevtoolsPlugin } from "@tanstack/react-form-devtools"; - - function App() { - return ; - } - `), - ) + expect(result.transformed).toBe(false) }) - test('should add function plugin alongside JSX plugins', () => { + test('should handle TanStackDevtools with children', () => { const code = ` import { TanStackDevtools } from '@tanstack/react-devtools' - import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools' - + function App() { - return } - ]} /> + return ( + +
Custom content
+
+ ) } ` const result = testTransform( code, - '@tanstack/react-form-devtools', - 'react-form', + '@tanstack/react-query-devtools', + 'TanStack Query', { - importName: 'FormDevtoolsPlugin', - type: 'function', + importName: 'ReactQueryDevtoolsPanel', + type: 'jsx', }, ) expect(result.transformed).toBe(true) - expect(removeEmptySpace(result.code)).toBe( - removeEmptySpace(` - import { TanStackDevtools } from '@tanstack/react-devtools'; - import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools'; - import { FormDevtoolsPlugin } from "@tanstack/react-form-devtools"; - - function App() { - return }, - FormDevtoolsPlugin() - ]} />; - } - `), - ) + expectContains(result.code, [ + ' }]}', + '
Custom content
', + ]) }) - test('should create plugins prop with function plugin when it does not exist', () => { + test('should handle multiple TanStackDevtools in same file', () => { const code = ` import { TanStackDevtools } from '@tanstack/react-devtools' - + function App() { - return + return ( + <> + + + + ) } ` const result = testTransform( code, - '@tanstack/react-form-devtools', - 'react-form', + '@tanstack/react-query-devtools', + 'TanStack Query', { - importName: 'FormDevtoolsPlugin', - type: 'function', + importName: 'ReactQueryDevtoolsPanel', + type: 'jsx', }, ) expect(result.transformed).toBe(true) - expect(removeEmptySpace(result.code)).toBe( - removeEmptySpace(` - import { TanStackDevtools } from '@tanstack/react-devtools'; - import { FormDevtoolsPlugin } from "@tanstack/react-form-devtools"; - - function App() { - return ; - } - `), - ) + const occurrences = + (removeEmptySpace(result.code).match( + new RegExp( + removeEmptySpace( + '{ name: "TanStack Query", render: }', + ), + 'g', + ), + ) || []).length + expect(occurrences).toBe(2) }) - test('should not add function plugin if it already exists', () => { + test('should handle TanStackDevtools deeply nested', () => { const code = ` import { TanStackDevtools } from '@tanstack/react-devtools' - import { FormDevtoolsPlugin } from '@tanstack/react-form-devtools' - + function App() { - return + return ( +
+
+ +
+
+ ) } ` const result = testTransform( code, - '@tanstack/react-form-devtools', - 'react-form', + '@tanstack/react-query-devtools', + 'TanStack Query', { - importName: 'FormDevtoolsPlugin', - type: 'function', + importName: 'ReactQueryDevtoolsPanel', + type: 'jsx', }, ) - expect(result.transformed).toBe(false) + expect(result.transformed).toBe(true) + expectContains(result.code, [ + '