From 9902a70da8d9a9765dd602b0cdaf3940a8bb1e9f Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 19 Sep 2025 12:20:31 +0200 Subject: [PATCH 1/3] feat: make the vite plugin remove devtools and its plugins completely --- examples/react/start/vite.config.ts | 1 - .../devtools-vite/src/remove-devtools.test.ts | 341 +++++++++++++++++- packages/devtools-vite/src/remove-devtools.ts | 111 +++++- 3 files changed, 431 insertions(+), 22 deletions(-) diff --git a/examples/react/start/vite.config.ts b/examples/react/start/vite.config.ts index b79af319..5c33afb7 100644 --- a/examples/react/start/vite.config.ts +++ b/examples/react/start/vite.config.ts @@ -23,7 +23,6 @@ const config = defineConfig({ Inspect(), tailwindcss(), tanstackStart({ - customViteReactPlugin: true, }), viteReact(), ], diff --git a/packages/devtools-vite/src/remove-devtools.test.ts b/packages/devtools-vite/src/remove-devtools.test.ts index 474b3698..9c186b54 100644 --- a/packages/devtools-vite/src/remove-devtools.test.ts +++ b/packages/devtools-vite/src/remove-devtools.test.ts @@ -57,22 +57,20 @@ export default function DevtoolsExample() { ) expect(output).toBe( removeEmptySpace(` - import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools'; - import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'; - import { + import { Link, Outlet, RouterProvider, createRootRoute, createRoute, createRouter - } from '@tanstack/react-router'; + } from '@tanstack/react-router'; export default function DevtoolsExample() { - return <> + return (<> - ; + ); } @@ -131,9 +129,7 @@ export default function DevtoolsExample() { ) expect(output).toBe( removeEmptySpace(` - import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools'; - import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'; - import { + import { Link, Outlet, RouterProvider, @@ -144,9 +140,9 @@ export default function DevtoolsExample() { export default function DevtoolsExample() { - return <> + return ( <> - ; + ); } @@ -205,9 +201,7 @@ export default function DevtoolsExample() { ) expect(output).toBe( removeEmptySpace(` - import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools'; - import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'; - import { + import { Link, Outlet, RouterProvider, @@ -218,13 +212,328 @@ export default function DevtoolsExample() { export default function DevtoolsExample() { - return <> + return (<> - ; + ); } `), ) }) + + test("it removes devtools and all possible variations of the plugins", () => { + const output = removeEmptySpace(removeDevtools(` + import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools' +import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools' +import { + Link, + Outlet, + RouterProvider, + createRootRoute, + createRoute, + createRouter, +} from '@tanstack/react-router' +import * as Tools from '@tanstack/react-devtools' + + + +export default function DevtoolsExample() { + return ( + <> + , + }, + { + name: 'TanStack Query', + render: () => , + }, + { + name: 'TanStack Router', + render: TanStackRouterDevtoolsPanel, + }, + some() + ]} + /> + + + ) +}`, 'test.jsx').code) + + expect(output).toBe( + removeEmptySpace(` +import { + Link, + Outlet, + RouterProvider, + createRootRoute, + createRoute, + createRouter +} from '@tanstack/react-router' ; + + + +export default function DevtoolsExample() { + return ( + <> + + + ); +} + `) + ) + }) + + describe("removing plugin imports", () => { + test("it removes the plugin import from the import array if multiple import identifiers exist", () => { + const output = removeEmptySpace(removeDevtools(` + import { ReactQueryDevtoolsPanel, test } from '@tanstack/react-query-devtools' + +import * as Tools from '@tanstack/react-devtools' + + + +export default function DevtoolsExample() { + return ( + <> + , + } + ]} + /> + + + ) +}`, 'test.jsx').code) + + expect(output).toBe( + removeEmptySpace(` + import { test } from '@tanstack/react-query-devtools'; + +export default function DevtoolsExample() { + return ( + <> + + + ); +} + `) + ) + + + }) + + test("it doesn't remove the whole import if imported with * as", () => { + const output = removeEmptySpace(removeDevtools(` + import * as Stuff from '@tanstack/react-query-devtools' + +import * as Tools from '@tanstack/react-devtools' + + + +export default function DevtoolsExample() { + return ( + <> + , + } + ]} + /> + + + ) +}`, 'test.jsx').code) + + expect(output).toBe( + removeEmptySpace(` + import * as Stuff from '@tanstack/react-query-devtools'; + +export default function DevtoolsExample() { + return ( + <> + + + ); +} + `) + ) + + + }) + + test("it removes the import completely if nothing is left", () => { + const output = removeEmptySpace(removeDevtools(` + import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools' +import * as Tools from '@tanstack/react-devtools' + +export default function DevtoolsExample() { + return ( + <> + , + } + ]} + /> + + + ) +}`, 'test.jsx').code) + + expect(output).toBe( + removeEmptySpace(` +export default function DevtoolsExample() { + return ( + <> + + + ); +} + `) + ) + + + }) + + test("it removes the import completely even if used as a function instead of jsx", () => { + const output = removeEmptySpace(removeDevtools(` + import { plugin } from '@tanstack/react-query-devtools' +import * as Tools from '@tanstack/react-devtools' + +export default function DevtoolsExample() { + return ( + <> + + + + ) +}`, 'test.jsx').code) + + expect(output).toBe( + removeEmptySpace(` +export default function DevtoolsExample() { + return ( + <> + + + ); +} + `) + ) + + + }) + + test("it removes the import completely even if used as a function inside of render", () => { + const output = removeEmptySpace(removeDevtools(` + import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools' +import * as Tools from '@tanstack/react-devtools' + +export default function DevtoolsExample() { + return ( + <> + + } + ]} + /> + + + ) +}`, 'test.jsx').code) + + expect(output).toBe( + removeEmptySpace(` +export default function DevtoolsExample() { + return ( + <> + + + ); +} + `) + ) + + + }) + + test("it removes the import completely even if used as a reference inside of render", () => { + const output = removeEmptySpace(removeDevtools(` + import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools' +import * as Tools from '@tanstack/react-devtools' + +export default function DevtoolsExample() { + return ( + <> + + + + ) +}`, 'test.jsx').code) + + expect(output).toBe( + removeEmptySpace(` +export default function DevtoolsExample() { + return ( + <> + + + ); +} + `) + ) + + + }) + + }) }) diff --git a/packages/devtools-vite/src/remove-devtools.ts b/packages/devtools-vite/src/remove-devtools.ts index 4efae6da..b803287b 100644 --- a/packages/devtools-vite/src/remove-devtools.ts +++ b/packages/devtools-vite/src/remove-devtools.ts @@ -1,6 +1,6 @@ import { gen, parse, trav } from './babel' import type { t } from './babel' -import type { types as Babel } from '@babel/core' +import type { types as Babel, NodePath } from '@babel/core' import type { ParseResult } from '@babel/parser' const isTanStackDevtoolsImport = (source: string) => @@ -12,9 +12,69 @@ const getImportedNames = (importDecl: t.ImportDeclaration) => { return importDecl.specifiers.map((spec) => spec.local.name) } +const getLeftoverImports = (node: NodePath) => { + const finalReferences: Array = [] + node.traverse({ + JSXAttribute(path) { + const node = path.node; + const propName = typeof node.name.name === "string" ? node.name.name : node.name.name.name; + + if (propName === 'plugins' && node.value?.type === "JSXExpressionContainer" && node.value.expression.type === "ArrayExpression") { + const elements = node.value.expression.elements; + + elements.forEach((el) => { + if (el?.type === "ObjectExpression") { + // { name: "something", render: ()=> } + const props = el.properties; + const referencesToRemove = props.map((prop) => { + if (prop.type === "ObjectProperty" && prop.key.type === "Identifier" && prop.key.name === "render") { + const value = prop.value; + // handle + if (value.type === "JSXElement" && value.openingElement.name.type === "JSXIdentifier") { + const elementName = value.openingElement.name.name; + return elementName + } + // handle () => or function() { return } + if (value.type === "ArrowFunctionExpression" || value.type === "FunctionExpression") { + const body = value.body; + if (body.type === "JSXElement" && body.openingElement.name.type === "JSXIdentifier") { + const elementName = body.openingElement.name.name; + return elementName + } + } + // handle render: SomeComponent + if (value.type === "Identifier") { + const elementName = value.name; + return elementName + } + + // handle render: someFunction() + if (value.type === "CallExpression" && value.callee.type === "Identifier") { + const elementName = value.callee.name; + return elementName + } + + return ""; + } + return ""; + }).filter(Boolean); + finalReferences.push(...referencesToRemove) + + } + }); + + } + }, + }) + return finalReferences; +} + const transform = (ast: ParseResult) => { let didTransform = false const devtoolsComponentNames = new Set() + const finalReferences: Array = [] + + const transformations: Array<() => void> = [] trav(ast, { ImportDeclaration(path) { @@ -23,7 +83,11 @@ const transform = (ast: ParseResult) => { getImportedNames(path.node).forEach((name) => devtoolsComponentNames.add(name), ) - path.remove() + + transformations.push(() => { + path.remove() + }) + didTransform = true } }, @@ -33,21 +97,57 @@ const transform = (ast: ParseResult) => { opening.name.type === 'JSXIdentifier' && devtoolsComponentNames.has(opening.name.name) ) { - path.remove() + const refs = getLeftoverImports(path) + + finalReferences.push(...refs) + transformations.push(() => { + path.remove() + }) didTransform = true } - if ( opening.name.type === 'JSXMemberExpression' && opening.name.object.type === 'JSXIdentifier' && devtoolsComponentNames.has(opening.name.object.name) ) { - path.remove() + const refs = getLeftoverImports(path) + finalReferences.push(...refs) + transformations.push(() => { + path.remove() + }) didTransform = true } }, }) + + trav(ast, { + ImportDeclaration(path) { + const imports = path.node.specifiers + for (const imported of imports) { + if (imported.type === "ImportSpecifier") { + if (finalReferences.includes(imported.local.name)) { + transformations.push(() => { + // remove the specifier + path.node.specifiers = path.node.specifiers.filter( + (spec) => spec !== imported + ) + // remove whole import if nothing is left + if (path.node.specifiers.length === 0) { + path.remove() + + } + }) + } + } + } + + }, + }) + + + transformations.forEach((fn) => fn()) + return didTransform } @@ -65,6 +165,7 @@ export function removeDevtools(code: string, id: string) { } return gen(ast, { sourceMaps: true, + retainLines: true, filename: id, sourceFileName: filePath, }) From 886a6158b48b1653e1c53769605b83bb2e7f16ef Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 19 Sep 2025 10:24:18 +0000 Subject: [PATCH 2/3] ci: apply automated fixes --- examples/react/start/vite.config.ts | 3 +- .../devtools-vite/src/remove-devtools.test.ts | 104 ++++++++++------- packages/devtools-vite/src/remove-devtools.ts | 107 +++++++++++------- 3 files changed, 127 insertions(+), 87 deletions(-) diff --git a/examples/react/start/vite.config.ts b/examples/react/start/vite.config.ts index 5c33afb7..35cd6a92 100644 --- a/examples/react/start/vite.config.ts +++ b/examples/react/start/vite.config.ts @@ -22,8 +22,7 @@ const config = defineConfig({ }), Inspect(), tailwindcss(), - tanstackStart({ - }), + tanstackStart({}), viteReact(), ], }) diff --git a/packages/devtools-vite/src/remove-devtools.test.ts b/packages/devtools-vite/src/remove-devtools.test.ts index 9c186b54..925b0a4f 100644 --- a/packages/devtools-vite/src/remove-devtools.test.ts +++ b/packages/devtools-vite/src/remove-devtools.test.ts @@ -222,8 +222,10 @@ export default function DevtoolsExample() { ) }) - test("it removes devtools and all possible variations of the plugins", () => { - const output = removeEmptySpace(removeDevtools(` + test('it removes devtools and all possible variations of the plugins', () => { + const output = removeEmptySpace( + removeDevtools( + ` import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools' import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools' import { @@ -264,7 +266,10 @@ export default function DevtoolsExample() { ) -}`, 'test.jsx').code) +}`, + 'test.jsx', + ).code, + ) expect(output).toBe( removeEmptySpace(` @@ -286,13 +291,15 @@ export default function DevtoolsExample() { ); } - `) + `), ) }) - describe("removing plugin imports", () => { - test("it removes the plugin import from the import array if multiple import identifiers exist", () => { - const output = removeEmptySpace(removeDevtools(` + describe('removing plugin imports', () => { + test('it removes the plugin import from the import array if multiple import identifiers exist', () => { + const output = removeEmptySpace( + removeDevtools( + ` import { ReactQueryDevtoolsPanel, test } from '@tanstack/react-query-devtools' import * as Tools from '@tanstack/react-devtools' @@ -316,7 +323,10 @@ export default function DevtoolsExample() { ) -}`, 'test.jsx').code) +}`, + 'test.jsx', + ).code, + ) expect(output).toBe( removeEmptySpace(` @@ -329,14 +339,14 @@ export default function DevtoolsExample() { ); } - `) + `), ) - - }) test("it doesn't remove the whole import if imported with * as", () => { - const output = removeEmptySpace(removeDevtools(` + const output = removeEmptySpace( + removeDevtools( + ` import * as Stuff from '@tanstack/react-query-devtools' import * as Tools from '@tanstack/react-devtools' @@ -360,7 +370,10 @@ export default function DevtoolsExample() { ) -}`, 'test.jsx').code) +}`, + 'test.jsx', + ).code, + ) expect(output).toBe( removeEmptySpace(` @@ -373,14 +386,14 @@ export default function DevtoolsExample() { ); } - `) + `), ) - - }) - test("it removes the import completely if nothing is left", () => { - const output = removeEmptySpace(removeDevtools(` + test('it removes the import completely if nothing is left', () => { + const output = removeEmptySpace( + removeDevtools( + ` import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools' import * as Tools from '@tanstack/react-devtools' @@ -401,7 +414,10 @@ export default function DevtoolsExample() { ) -}`, 'test.jsx').code) +}`, + 'test.jsx', + ).code, + ) expect(output).toBe( removeEmptySpace(` @@ -412,14 +428,14 @@ export default function DevtoolsExample() { ); } - `) + `), ) - - }) - test("it removes the import completely even if used as a function instead of jsx", () => { - const output = removeEmptySpace(removeDevtools(` + test('it removes the import completely even if used as a function instead of jsx', () => { + const output = removeEmptySpace( + removeDevtools( + ` import { plugin } from '@tanstack/react-query-devtools' import * as Tools from '@tanstack/react-devtools' @@ -440,7 +456,10 @@ export default function DevtoolsExample() { ) -}`, 'test.jsx').code) +}`, + 'test.jsx', + ).code, + ) expect(output).toBe( removeEmptySpace(` @@ -451,14 +470,14 @@ export default function DevtoolsExample() { ); } - `) + `), ) - - }) - test("it removes the import completely even if used as a function inside of render", () => { - const output = removeEmptySpace(removeDevtools(` + test('it removes the import completely even if used as a function inside of render', () => { + const output = removeEmptySpace( + removeDevtools( + ` import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools' import * as Tools from '@tanstack/react-devtools' @@ -479,7 +498,10 @@ export default function DevtoolsExample() { ) -}`, 'test.jsx').code) +}`, + 'test.jsx', + ).code, + ) expect(output).toBe( removeEmptySpace(` @@ -490,14 +512,14 @@ export default function DevtoolsExample() { ); } - `) + `), ) - - }) - test("it removes the import completely even if used as a reference inside of render", () => { - const output = removeEmptySpace(removeDevtools(` + test('it removes the import completely even if used as a reference inside of render', () => { + const output = removeEmptySpace( + removeDevtools( + ` import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools' import * as Tools from '@tanstack/react-devtools' @@ -518,7 +540,10 @@ export default function DevtoolsExample() { ) -}`, 'test.jsx').code) +}`, + 'test.jsx', + ).code, + ) expect(output).toBe( removeEmptySpace(` @@ -529,11 +554,8 @@ export default function DevtoolsExample() { ); } - `) + `), ) - - }) - }) }) diff --git a/packages/devtools-vite/src/remove-devtools.ts b/packages/devtools-vite/src/remove-devtools.ts index b803287b..30c32b4c 100644 --- a/packages/devtools-vite/src/remove-devtools.ts +++ b/packages/devtools-vite/src/remove-devtools.ts @@ -16,57 +16,80 @@ const getLeftoverImports = (node: NodePath) => { const finalReferences: Array = [] node.traverse({ JSXAttribute(path) { - const node = path.node; - const propName = typeof node.name.name === "string" ? node.name.name : node.name.name.name; + const node = path.node + const propName = + typeof node.name.name === 'string' + ? node.name.name + : node.name.name.name - if (propName === 'plugins' && node.value?.type === "JSXExpressionContainer" && node.value.expression.type === "ArrayExpression") { - const elements = node.value.expression.elements; + if ( + propName === 'plugins' && + node.value?.type === 'JSXExpressionContainer' && + node.value.expression.type === 'ArrayExpression' + ) { + const elements = node.value.expression.elements elements.forEach((el) => { - if (el?.type === "ObjectExpression") { + if (el?.type === 'ObjectExpression') { // { name: "something", render: ()=> } - const props = el.properties; - const referencesToRemove = props.map((prop) => { - if (prop.type === "ObjectProperty" && prop.key.type === "Identifier" && prop.key.name === "render") { - const value = prop.value; - // handle - if (value.type === "JSXElement" && value.openingElement.name.type === "JSXIdentifier") { - const elementName = value.openingElement.name.name; - return elementName - } - // handle () => or function() { return } - if (value.type === "ArrowFunctionExpression" || value.type === "FunctionExpression") { - const body = value.body; - if (body.type === "JSXElement" && body.openingElement.name.type === "JSXIdentifier") { - const elementName = body.openingElement.name.name; + const props = el.properties + const referencesToRemove = props + .map((prop) => { + if ( + prop.type === 'ObjectProperty' && + prop.key.type === 'Identifier' && + prop.key.name === 'render' + ) { + const value = prop.value + // handle + if ( + value.type === 'JSXElement' && + value.openingElement.name.type === 'JSXIdentifier' + ) { + const elementName = value.openingElement.name.name + return elementName + } + // handle () => or function() { return } + if ( + value.type === 'ArrowFunctionExpression' || + value.type === 'FunctionExpression' + ) { + const body = value.body + if ( + body.type === 'JSXElement' && + body.openingElement.name.type === 'JSXIdentifier' + ) { + const elementName = body.openingElement.name.name + return elementName + } + } + // handle render: SomeComponent + if (value.type === 'Identifier') { + const elementName = value.name return elementName } - } - // handle render: SomeComponent - if (value.type === "Identifier") { - const elementName = value.name; - return elementName - } - // handle render: someFunction() - if (value.type === "CallExpression" && value.callee.type === "Identifier") { - const elementName = value.callee.name; - return elementName - } + // handle render: someFunction() + if ( + value.type === 'CallExpression' && + value.callee.type === 'Identifier' + ) { + const elementName = value.callee.name + return elementName + } - return ""; - } - return ""; - }).filter(Boolean); + return '' + } + return '' + }) + .filter(Boolean) finalReferences.push(...referencesToRemove) - } - }); - + }) } }, }) - return finalReferences; + return finalReferences } const transform = (ast: ParseResult) => { @@ -120,32 +143,28 @@ const transform = (ast: ParseResult) => { }, }) - trav(ast, { ImportDeclaration(path) { const imports = path.node.specifiers for (const imported of imports) { - if (imported.type === "ImportSpecifier") { + if (imported.type === 'ImportSpecifier') { if (finalReferences.includes(imported.local.name)) { transformations.push(() => { // remove the specifier path.node.specifiers = path.node.specifiers.filter( - (spec) => spec !== imported + (spec) => spec !== imported, ) // remove whole import if nothing is left if (path.node.specifiers.length === 0) { path.remove() - } }) } } } - }, }) - transformations.forEach((fn) => fn()) return didTransform From 84e2359d9cabf0ed99d6bd1d5c70f8450fb5699b Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 19 Sep 2025 12:41:41 +0200 Subject: [PATCH 3/3] changeset --- .changeset/plenty-lions-camp.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/plenty-lions-camp.md diff --git a/.changeset/plenty-lions-camp.md b/.changeset/plenty-lions-camp.md new file mode 100644 index 00000000..4ceb1dc8 --- /dev/null +++ b/.changeset/plenty-lions-camp.md @@ -0,0 +1,6 @@ +--- +'@tanstack/devtools-vite': patch +'@tanstack/devtools': patch +--- + +improve devtools removal and fix issues with css