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 diff --git a/examples/react/start/vite.config.ts b/examples/react/start/vite.config.ts index b79af319..35cd6a92 100644 --- a/examples/react/start/vite.config.ts +++ b/examples/react/start/vite.config.ts @@ -22,9 +22,7 @@ const config = defineConfig({ }), Inspect(), tailwindcss(), - tanstackStart({ - customViteReactPlugin: true, - }), + tanstackStart({}), viteReact(), ], }) diff --git a/packages/devtools-vite/src/remove-devtools.test.ts b/packages/devtools-vite/src/remove-devtools.test.ts index 474b3698..925b0a4f 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,350 @@ 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..30c32b4c 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,92 @@ 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 +106,11 @@ const transform = (ast: ParseResult) => { getImportedNames(path.node).forEach((name) => devtoolsComponentNames.add(name), ) - path.remove() + + transformations.push(() => { + path.remove() + }) + didTransform = true } }, @@ -33,21 +120,53 @@ 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 +184,7 @@ export function removeDevtools(code: string, id: string) { } return gen(ast, { sourceMaps: true, + retainLines: true, filename: id, sourceFileName: filePath, })