diff --git a/_artifacts/domain_map.yaml b/_artifacts/domain_map.yaml index c92dedf9..e1b4d8b0 100644 --- a/_artifacts/domain_map.yaml +++ b/_artifacts/domain_map.yaml @@ -231,7 +231,7 @@ skills: - mistake: 'Source injection on spread props elements' mechanism: > - The Babel transform skips elements with {...props} spread to avoid + The Oxc transform skips elements with {...props} spread to avoid overwriting dynamic attributes. Agent might not realize source inspector wont work on those elements. source: 'packages/devtools-vite/src/inject-source.ts' diff --git a/docs/architecture.md b/docs/architecture.md index c9065d87..2cd08a6b 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -194,7 +194,7 @@ Adapters do not re-implement the devtools UI, manage settings, handle events, or `@tanstack/devtools-vite` is a collection of Vite plugins that enhance the development experience and clean up production builds. It returns an array of Vite plugins, each handling a specific concern: ### Source injection (`@tanstack/devtools:inject-source`) -Uses Babel to parse JSX/TSX files and injects `data-tsd-source` attributes on every JSX element. These attributes encode the file path, line number, and column number of each element in source code, which the source inspector feature uses to implement click-to-open-in-editor. +Uses Oxc to parse JSX/TSX files and injects `data-tsd-source` attributes on every JSX element. These attributes encode the file path, line number, and column number of each element in source code, which the source inspector feature uses to implement click-to-open-in-editor. ### Server event bus (`@tanstack/devtools:custom-server`) Starts a `ServerEventBus` on the Vite dev server. Also sets up middleware for the go-to-source editor integration and bidirectional console piping (client logs appear in the terminal, server logs appear in the browser). diff --git a/docs/source-inspector.md b/docs/source-inspector.md index e49535db..ed1ec491 100644 --- a/docs/source-inspector.md +++ b/docs/source-inspector.md @@ -18,13 +18,13 @@ The feature only works in development. In production builds, source attributes a ```mermaid flowchart LR - A["Your JSX/TSX files"] -- "Babel transform" --> B["data-tsd-source
attributes injected"] + A["Your JSX/TSX files"] -- "Oxc transform" --> B["data-tsd-source
attributes injected"] B -- "Hold inspect hotkey
+ click element" --> C["Devtools reads
data-tsd-source"] C -- "HTTP request" --> D["Vite dev server"] D -- "launch-editor" --> E["Opens file in editor
at exact line"] ``` -The Vite plugin uses Babel to parse your JSX/TSX files during development. It adds a `data-tsd-source="filepath:line:column"` attribute to every JSX element. When you activate the source inspector and click an element, the devtools reads this attribute and sends a request to the Vite dev server. The server then launches your editor at the specified file and line using `launch-editor`. +The Vite plugin uses Oxc to parse your JSX/TSX files during development. It adds a `data-tsd-source="filepath:line:column"` attribute to every JSX element. When you activate the source inspector and click an element, the devtools reads this attribute and sends a request to the Vite dev server. The server then launches your editor at the specified file and line using `launch-editor`. ## Activating the Inspector diff --git a/docs/vite-plugin.md b/docs/vite-plugin.md index fa9a4ecb..5ba2181d 100644 --- a/docs/vite-plugin.md +++ b/docs/vite-plugin.md @@ -73,7 +73,7 @@ export default { ### editor -> [!IMPORTANT] +> [!IMPORTANT] > `editor` is used as an escape hatch to implement your own go-to-source functionality if your system/editor does not work OOTB. We use `launch-editor` under the hood which supports a lot of editors but not all. If your editor is not supported you can implement your own version here. Here is the list of supported editors: https://github.com/yyx990803/launch-editor?tab=readme-ov-file#supported-editors The open in editor configuration which has two fields, `name` and `open`, @@ -209,7 +209,7 @@ The Vite plugin is composed of several sub-plugins, each handling a specific con ```mermaid graph TD vite["@tanstack/devtools-vite"] - vite --> source["Source Injection
Babel → data-tsd-source attrs"] + vite --> source["Source Injection
Oxc → data-tsd-source attrs"] vite --> server["Server Event Bus
WebSocket + SSE transport"] vite --> strip["Production Stripping
Remove devtools on build"] vite --> pipe["Console Piping
Client ↔ Server logs"] @@ -220,7 +220,7 @@ graph TD ### Go to Source -The "Go to Source" feature lets you click on any element in your browser and open its source file in your editor at the exact line where it's defined. It works by injecting `data-tsd-source` attributes into your components via a Babel transformation during development. These attributes encode the file path and line number of each element. +The "Go to Source" feature lets you click on any element in your browser and open its source file in your editor at the exact line where it's defined. It works by injecting `data-tsd-source` attributes into your components via an Oxc transformation during development. These attributes encode the file path and line number of each element. To use it, activate the source inspector by holding the inspect hotkey (default: Shift+Alt+Ctrl/Meta). An overlay will highlight elements under your cursor and display their source location. Clicking on a highlighted element opens the corresponding file in your editor at the exact line, powered by `launch-editor` under the hood. diff --git a/packages/devtools-vite/package.json b/packages/devtools-vite/package.json index 83f4a6f6..18be4b79 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.121.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/skills/devtools-vite-plugin/SKILL.md b/packages/devtools-vite/skills/devtools-vite-plugin/SKILL.md index a31b9c65..cceba416 100644 --- a/packages/devtools-vite/skills/devtools-vite-plugin/SKILL.md +++ b/packages/devtools-vite/skills/devtools-vite-plugin/SKILL.md @@ -67,13 +67,13 @@ From `packages/devtools-vite/src/index.ts`: | Sub-plugin name | What it does | When active | | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------- | -| `@tanstack/devtools:inject-source` | Babel transform adding `data-tsd-source` attrs to JSX | dev mode + `injectSource.enabled` | +| `@tanstack/devtools:inject-source` | Oxc transform adding `data-tsd-source` attrs to JSX | dev mode + `injectSource.enabled` | | `@tanstack/devtools:config` | Reserved for future config modifications | serve command only | | `@tanstack/devtools:custom-server` | Starts ServerEventBus, registers middleware for open-source/console-pipe endpoints | dev mode | | `@tanstack/devtools:remove-devtools-on-build` | Strips devtools imports/JSX from production bundles | build command or production mode + `removeDevtoolsOnBuild` | | `@tanstack/devtools:event-client-setup` | Marketplace: listens for install/add-plugin events via devtoolsEventClient | dev mode + serve + not CI | | `@tanstack/devtools:console-pipe-transform` | Injects runtime console-pipe code into entry files | dev mode + serve + `consolePiping.enabled` | -| `@tanstack/devtools:better-console-logs` | Babel transform prepending source location to `console.log`/`console.error` | dev mode + `enhancedLogs.enabled` | +| `@tanstack/devtools:better-console-logs` | Oxc transform prepending source location to `console.log`/`console.error` | dev mode + `enhancedLogs.enabled` | | `@tanstack/devtools:inject-plugin` | Detects which file imports TanStackDevtools (for marketplace injection) | dev mode + serve | | `@tanstack/devtools:connection-injection` | Replaces `__TANSTACK_DEVTOOLS_PORT__`, `__TANSTACK_DEVTOOLS_HOST__`, `__TANSTACK_DEVTOOLS_PROTOCOL__` placeholders | dev mode + serve | @@ -81,7 +81,7 @@ From `packages/devtools-vite/src/index.ts`: ### Source Injection -Adds `data-tsd-source="::"` attributes to every JSX opening element via Babel. This powers the "Go to Source" feature -- hold the inspect hotkey (default: Shift+Alt+Ctrl/Meta), hover over elements, click to open in editor. +Adds `data-tsd-source="::"` attributes to every JSX opening element via Oxc. This powers the "Go to Source" feature -- hold the inspect hotkey (default: Shift+Alt+Ctrl/Meta), hover over elements, click to open in editor. **Key behaviors:** @@ -137,7 +137,7 @@ devtools({ ### Enhanced Logging -Babel transform that prepends source location info to `console.log()` and `console.error()` calls. In the browser, this renders as a clickable "Go to Source" link. On the server, it shows `LOG ::` in chalk colors. +Oxc transform that prepends source location info to `console.log()` and `console.error()` calls. In the browser, this renders as a clickable "Go to Source" link. On the server, it shows `LOG ::` in chalk colors. The transform inserts a spread of a conditional expression: `...(typeof window === 'undefined' ? serverLogMessage : browserLogMessage)` as the first argument of the console call. @@ -261,7 +261,7 @@ Source injection, console piping, enhanced logging, the server event bus, and th ### 4. Source injection on spread-props elements (MEDIUM) -The Babel transform in `inject-source.ts` explicitly skips any JSX element that has a `{...props}` spread where `props` is the component's parameter name. This is intentional -- the spread would overwrite the injected `data-tsd-source` attribute. If source inspection doesn't work for a specific component, check if it spreads its props parameter. +The Oxc transform in `inject-source.ts` explicitly skips any JSX element that has a `{...props}` spread where `props` is the component's parameter name. This is intentional -- the spread would overwrite the injected `data-tsd-source` attribute. If source inspection doesn't work for a specific component, check if it spreads its props parameter. ```tsx // data-tsd-source will NOT be injected on
here @@ -301,8 +301,8 @@ These are registered on the Vite dev server (not the event bus server): ## Key Source Files - `packages/devtools-vite/src/plugin.ts` -- Main plugin factory with all sub-plugins and config type -- `packages/devtools-vite/src/inject-source.ts` -- Babel transform for data-tsd-source injection -- `packages/devtools-vite/src/enhance-logs.ts` -- Babel transform for enhanced console logs +- `packages/devtools-vite/src/inject-source.ts` -- Oxc transform for data-tsd-source injection +- `packages/devtools-vite/src/enhance-logs.ts` -- Oxc transform for enhanced console logs - `packages/devtools-vite/src/remove-devtools.ts` -- Production stripping transform - `packages/devtools-vite/src/virtual-console.ts` -- Console pipe runtime code generator - `packages/devtools-vite/src/editor.ts` -- Editor config type and launch-editor integration diff --git a/packages/devtools-vite/skills/devtools-vite-plugin/references/vite-options.md b/packages/devtools-vite/skills/devtools-vite-plugin/references/vite-options.md index 09fd7062..aa80bce4 100644 --- a/packages/devtools-vite/skills/devtools-vite-plugin/references/vite-options.md +++ b/packages/devtools-vite/skills/devtools-vite-plugin/references/vite-options.md @@ -43,7 +43,7 @@ declare function defineDevtoolsConfig( ## `injectSource` -Controls source injection -- the Babel transform that adds `data-tsd-source` attributes to JSX elements for the "Go to Source" feature. +Controls source injection -- the Oxc transform that adds `data-tsd-source` attributes to JSX elements for the "Go to Source" feature. | Field | Type | Default | Description | | ------------------- | ------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -117,7 +117,7 @@ devtools({ ## `enhancedLogs` -Controls the Babel transform that prepends source location information to `console.log()` and `console.error()` calls. +Controls the Oxc transform that prepends source location information to `console.log()` and `console.error()` calls. | Field | Type | Default | Description | | --------- | --------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 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, [ + '
; + ) } `), ) @@ -316,9 +316,9 @@ function test(props) { expect(output).toBe( removeEmptySpace(` function test({...props}) { - return
+ return (
; +
) } `), ) @@ -340,9 +340,9 @@ function test({...props}) { expect(output).toBe( removeEmptySpace(` function test({...rest}) { - return
+ return (
; +
) } `), ) @@ -374,7 +374,7 @@ function test({...rest}) { expect(output).toBe( removeEmptySpace(` function test({ ...props }) { - return