import MagicString from 'magic-string' import { parseSync } from 'oxc-parser' import { walk } from './ast-utils' import { isTanStackDevtoolsImport } from './devtools-packages' import type { JSXOpeningElement } from 'oxc-parser' /** * Extract component names referenced in the `plugins` prop of a devtools element. */ function getPluginReferences(opening: JSXOpeningElement): Array { const refs: Array = [] for (const attr of opening.attributes) { if (attr.type !== 'JSXAttribute') continue if (attr.name.type !== 'JSXIdentifier' || attr.name.name !== 'plugins') continue if ( !attr.value || attr.value.type !== 'JSXExpressionContainer' || attr.value.expression.type !== 'ArrayExpression' ) continue for (const el of attr.value.expression.elements) { if (!el || el.type !== 'ObjectExpression') continue for (const prop of el.properties) { if ( prop.type !== 'Property' || prop.key.type !== 'Identifier' || prop.key.name !== 'render' ) continue const value = prop.value // handle if ( value.type === 'JSXElement' && value.openingElement.name.type === 'JSXIdentifier' ) { refs.push(value.openingElement.name.name) } // handle () => else if ( value.type === 'ArrowFunctionExpression' || value.type === 'FunctionExpression' ) { const body = value.body if ( body && body.type === 'JSXElement' && body.openingElement.name.type === 'JSXIdentifier' ) { refs.push(body.openingElement.name.name) } } // handle render: SomeComponent else if (value.type === 'Identifier') { refs.push(value.name) } // handle render: someFunction() else if ( value.type === 'CallExpression' && value.callee.type === 'Identifier' ) { refs.push(value.callee.name) } } } } return refs } export function removeDevtools(code: string, id: string) { const filePath = id.split('?')[0]! try { const result = parseSync(filePath, code, { sourceType: 'module', lang: 'tsx', }) if (result.errors.length > 0) return const s = new MagicString(code) const devtoolsNames = new Set() const pluginRefs: Array = [] // Pass 1: Collect devtools import names and mark for removal walk(result.program, (node) => { if ( node.type === 'ImportDeclaration' && isTanStackDevtoolsImport(node.source.value) ) { for (const spec of node.specifiers) { devtoolsNames.add(spec.local.name) } let end = node.end if (code[end] === '\n') end++ s.remove(node.start, end) } }) if (devtoolsNames.size === 0) return // Pass 2: Find and remove devtools JSX elements, collect plugin references walk(result.program, (node, parentNode) => { if (node.type !== 'JSXElement') return const opening = node.openingElement let matches = false if ( opening.name.type === 'JSXIdentifier' && devtoolsNames.has(opening.name.name) ) { matches = true } else if ( opening.name.type === 'JSXMemberExpression' && opening.name.object.type === 'JSXIdentifier' && devtoolsNames.has(opening.name.object.name) ) { matches = true } if (!matches) return pluginRefs.push(...getPluginReferences(opening)) let end = node.end if (code[end] === '\n') end++ /** * Devtools nodes can be removed safely when nested in JSX. * In all other contexts, replace them with `null` to avoid leaving invalid syntax. */ if ( parentNode?.type === 'JSXElement' || parentNode?.type === 'JSXFragment' ) { s.remove(node.start, end) } else { s.overwrite(node.start, end, 'null') } }) // Pass 3: Remove plugin imports that are no longer referenced if (pluginRefs.length > 0) { walk(result.program, (node) => { if (node.type !== 'ImportDeclaration') return if (isTanStackDevtoolsImport(node.source.value)) return const toRemove = node.specifiers.filter( (spec) => spec.type === 'ImportSpecifier' && pluginRefs.includes(spec.local.name), ) if (toRemove.length === 0) return const remaining = node.specifiers.filter( (spec) => !toRemove.includes(spec), ) if (remaining.length === 0) { let end = node.end if (code[end] === '\n') end++ s.remove(node.start, end) } else { // Rebuild import with remaining specifiers const specTexts = remaining.map((spec) => code.slice(spec.start, spec.end), ) const sourceText = code.slice(node.source.start, node.source.end) s.overwrite( node.start, node.end, `import { ${specTexts.join(', ')} } from ${sourceText}`, ) } }) } if (!s.hasChanged()) return return { code: s.toString(), map: s.generateMap({ source: filePath, file: id, includeContent: true, }), } } catch (e) { return } }