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 (
-
- setCount(count + 1)}>
- Count: {count}
-
-
-
- )
+ 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
- setCount(count + 1)}>
- Count: {count}
-
-
- }]} />
-
;
- }
- `),
- )
+ 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, [
+ '',
+ ' }]}',
+ ])
})
- test('should handle function plugin with renamed devtools import', () => {
+ test('should preserve existing code structure', () => {
const code = `
- import { TanStackDevtools as DevtoolsPanel } from '@tanstack/react-devtools'
-
+ import { TanStackDevtools } from '@tanstack/react-devtools'
+ import { useState } from 'react'
+
function App() {
- return
+ const [count, setCount] = useState(0)
+
+ return (
+
+ setCount(count + 1)}>
+ Count: {count}
+
+
+
+ )
}
`
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 as DevtoolsPanel } from '@tanstack/react-devtools';
- import { FormDevtoolsPlugin } from "@tanstack/react-form-devtools";
-
- function App() {
- return ;
- }
- `),
- )
+ expectContains(result.code, [
+ 'const [count, setCount] = useState(0)',
+ ' setCount(count + 1)}>',
+ ' }]}',
+ ])
})
- test('should handle function plugin with namespace import', () => {
+ test('should handle TypeScript code', () => {
const code = `
- import * as DevtoolsModule from '@tanstack/solid-devtools'
-
- function App() {
- return
+ import { TanStackDevtools } from '@tanstack/react-devtools'
+ import type { FC } from 'react'
+
+ const App: FC = () => {
+ return
}
`
const result = testTransform(
code,
- '@tanstack/solid-form-devtools',
- 'solid-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 * as DevtoolsModule from '@tanstack/solid-devtools';
- import { FormDevtoolsPlugin } from "@tanstack/solid-form-devtools";
-
- function App() {
- return ;
- }
- `),
- )
+ expectContains(result.code, [
+ 'import type { FC } from \'react\'',
+ '{ name: "TanStack Query", render: }',
+ ])
})
- test('should add multiple function plugins correctly', () => {
+ test('should handle plugins array with trailing comma', () => {
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-router-devtools',
- 'react-router',
+ '@tanstack/react-query-devtools',
+ 'TanStack Query',
{
- importName: 'RouterDevtoolsPlugin',
- 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';
- import { RouterDevtoolsPlugin } from "@tanstack/react-router-devtools";
-
- function App() {
- return ;
- }
- `),
- )
+ expectContains(result.code, [
+ '{ name: \'other\', render: },',
+ '{ name: "TanStack Query", render: }',
+ ])
})
- test('should not transform when pluginImport is not provided', () => {
+ test('should not transform if devtools import not found', () => {
const code = `
- import { TanStackDevtools } from '@tanstack/react-devtools'
-
+ import { SomeOtherComponent } from 'some-package'
+
function App() {
- return
+ return
}
`
- // No pluginImport provided - should return false
const result = testTransform(
code,
'@tanstack/react-query-devtools',
'TanStack Query',
{
- importName: '',
+ importName: 'ReactQueryDevtoolsPanel',
type: 'jsx',
},
)
diff --git a/packages/devtools-vite/src/inject-plugin.ts b/packages/devtools-vite/src/inject-plugin.ts
index 570ce667..7dffec93 100644
--- a/packages/devtools-vite/src/inject-plugin.ts
+++ b/packages/devtools-vite/src/inject-plugin.ts
@@ -1,307 +1,321 @@
import { readFileSync, writeFileSync } from 'node:fs'
-import { gen, parse, t, trav } from './babel'
+import { Visitor, parseSync } from 'oxc-parser'
import type { PluginInjection } from '@tanstack/devtools-client'
-import type { types as Babel } from '@babel/core'
-import type { ParseResult } from '@babel/parser'
+import type { ArrayExpressionElement, JSXElementName } from 'oxc-parser'
-/**
- * Detects if a file imports TanStack devtools packages
- * Handles: import X from '@tanstack/react-devtools'
- * import * as X from '@tanstack/react-devtools'
- * import { TanStackDevtools } from '@tanstack/react-devtools'
- */
-const detectDevtoolsImport = (code: string): boolean => {
- const devtoolsPackages = [
- '@tanstack/react-devtools',
- '@tanstack/solid-devtools',
- '@tanstack/vue-devtools',
- '@tanstack/svelte-devtools',
- '@tanstack/angular-devtools',
- ]
+type Edit = {
+ at: number
+ text: string
+}
- try {
- const ast = parse(code, {
- sourceType: 'module',
- plugins: ['jsx', 'typescript'],
+const devtoolsPackages = [
+ '@tanstack/react-devtools',
+ '@tanstack/preact-devtools',
+ '@tanstack/solid-devtools',
+ '@tanstack/vue-devtools',
+ '@tanstack/svelte-devtools',
+ '@tanstack/angular-devtools',
+]
+
+const parseModule = (code: string) => {
+ return parseSync('inject-plugin.tsx', code, {
+ sourceType: 'module',
+ lang: 'tsx',
+ range: true,
+ })
+}
+
+const applyEdits = (code: string, edits: Array) => {
+ const ordered = [...edits].sort((a, b) => b.at - a.at)
+ let next = code
+
+ for (const edit of ordered) {
+ next = next.slice(0, edit.at) + edit.text + next.slice(edit.at)
+ }
+
+ return next
+}
+
+const getJsxName = (name: JSXElementName): string => {
+ if (name.type === 'JSXIdentifier') {
+ return name.name
+ }
+
+ if (name.type === 'JSXMemberExpression') {
+ return `${getJsxName(name.object)}.${getJsxName(name.property)}`
+ }
+
+ return ''
+}
+
+const makePluginElement = (
+ pluginType: 'jsx' | 'function',
+ importName: string,
+ displayName: string,
+) => {
+ if (pluginType === 'function') {
+ return `${importName}()`
+ }
+
+ return `{ name: ${JSON.stringify(displayName)}, render: <${importName} /> }`
+}
+
+const isPluginAlreadyInArray = (
+ elements: Array,
+ pluginType: 'jsx' | 'function',
+ importName: string,
+ displayName: string,
+) => {
+ return elements.some((element) => {
+ if (!element) {
+ return false
+ }
+
+ if (pluginType === 'function') {
+ return (
+ element.type === 'CallExpression' &&
+ element.callee.type === 'Identifier' &&
+ element.callee.name === importName
+ )
+ }
+
+ if (element.type !== 'ObjectExpression') {
+ return false
+ }
+
+ return element.properties.some((prop) => {
+ if (prop.type !== 'Property') {
+ return false
+ }
+
+ const keyName =
+ prop.key.type === 'Identifier'
+ ? prop.key.name
+ : prop.key.type === 'Literal'
+ ? prop.key.value
+ : null
+
+ return (
+ keyName === 'name' &&
+ prop.value.type === 'Literal' &&
+ prop.value.value === displayName
+ )
})
+ })
+}
+const detectDevtoolsImport = (code: string): boolean => {
+ try {
+ const result = parseModule(code)
let hasDevtoolsImport = false
- trav(ast, {
- ImportDeclaration(path) {
- const importSource = path.node.source.value
- if (devtoolsPackages.includes(importSource)) {
+ new Visitor({
+ ImportDeclaration(node) {
+ if (hasDevtoolsImport) {
+ return
+ }
+
+ if (devtoolsPackages.includes(node.source.value)) {
hasDevtoolsImport = true
- path.stop()
}
},
- })
+ }).visit(result.program)
return hasDevtoolsImport
- } catch (e) {
+ } catch {
return false
}
}
+export function detectDevtoolsFile(code: string): boolean {
+ return detectDevtoolsImport(code)
+}
+
/**
- * Finds the TanStackDevtools component name in the file
- * Handles renamed imports and namespace imports
+ * Finds the TanStackDevtools component name in the file.
+ * Returns either `TanStackDevtools`, a renamed identifier, or `Namespace.TanStackDevtools`.
*/
-export const findDevtoolsComponentName = (
- ast: ParseResult,
-): string | null => {
- let componentName: string | null = null
- const devtoolsPackages = [
- '@tanstack/react-devtools',
- '@tanstack/solid-devtools',
- '@tanstack/vue-devtools',
- '@tanstack/svelte-devtools',
- '@tanstack/angular-devtools',
- ]
-
- trav(ast, {
- ImportDeclaration(path) {
- const importSource = path.node.source.value
- if (devtoolsPackages.includes(importSource)) {
- // Check for: import { TanStackDevtools } from '@tanstack/...'
- const namedImport = path.node.specifiers.find(
+export function findDevtoolsComponentName(code: string): string | null {
+ try {
+ const result = parseModule(code)
+ let componentName: string | null = null
+
+ new Visitor({
+ ImportDeclaration(node) {
+ if (componentName) {
+ return
+ }
+
+ if (!devtoolsPackages.includes(node.source.value)) {
+ return
+ }
+
+ const namedImport = node.specifiers.find(
(spec) =>
- t.isImportSpecifier(spec) &&
- t.isIdentifier(spec.imported) &&
+ spec.type === 'ImportSpecifier' &&
+ spec.imported.type === 'Identifier' &&
spec.imported.name === 'TanStackDevtools',
)
- if (namedImport && t.isImportSpecifier(namedImport)) {
+
+ if (namedImport && namedImport.type === 'ImportSpecifier') {
componentName = namedImport.local.name
- path.stop()
return
}
- // Check for: import * as DevtoolsName from '@tanstack/...'
- const namespaceImport = path.node.specifiers.find((spec) =>
- t.isImportNamespaceSpecifier(spec),
+ const namespaceImport = node.specifiers.find(
+ (spec) => spec.type === 'ImportNamespaceSpecifier',
)
- if (namespaceImport && t.isImportNamespaceSpecifier(namespaceImport)) {
- // For namespace imports, we need to look for DevtoolsName.TanStackDevtools
+
+ if (namespaceImport) {
componentName = `${namespaceImport.local.name}.TanStackDevtools`
- path.stop()
- return
}
- }
- },
- })
+ },
+ }).visit(result.program)
- return componentName
+ return componentName
+ } catch {
+ return null
+ }
}
-export const transformAndInject = (
- ast: ParseResult,
+export function transformAndInject(
+ code: string,
injection: PluginInjection,
devtoolsComponentName: string,
-) => {
- let didTransform = false
-
- // Use pluginImport if provided, otherwise generate from package name
+): { didTransform: boolean; code: string } {
const importName = injection.pluginImport?.importName
const pluginType = injection.pluginImport?.type || 'jsx'
const displayName = injection.pluginName
if (!importName) {
- return false
+ return { didTransform: false, code }
}
- // Handle namespace imports like DevtoolsModule.TanStackDevtools
- const isNamespaceImport = devtoolsComponentName.includes('.')
-
- // Find and modify the TanStackDevtools JSX element
- trav(ast, {
- JSXOpeningElement(path) {
- const elementName = path.node.name
- let matches = false
-
- if (isNamespaceImport) {
- // Handle
- if (t.isJSXMemberExpression(elementName)) {
- const fullName = `${t.isJSXIdentifier(elementName.object) ? elementName.object.name : ''}.${t.isJSXIdentifier(elementName.property) ? elementName.property.name : ''}`
- matches = fullName === devtoolsComponentName
- }
- } else {
- // Handle or
- matches =
- t.isJSXIdentifier(elementName) &&
- elementName.name === devtoolsComponentName
+
+ let ast
+ try {
+ ast = parseModule(code)
+ } catch {
+ return { didTransform: false, code }
+ }
+
+ const edits: Array = []
+
+ const importExists = ast.program.body.some((node) => {
+ if (node.type !== 'ImportDeclaration') {
+ return false
+ }
+
+ if (node.source.value !== injection.packageName) {
+ return false
+ }
+
+ return node.specifiers.some(
+ (spec) =>
+ spec.type === 'ImportSpecifier' && spec.local.name === importName,
+ )
+ })
+
+ new Visitor({
+ JSXOpeningElement(node) {
+ const fullName = getJsxName(node.name)
+ if (fullName !== devtoolsComponentName) {
+ return
}
- if (matches) {
- // Find the plugins prop
- const pluginsProp = path.node.attributes.find(
- (attr) =>
- t.isJSXAttribute(attr) &&
- t.isJSXIdentifier(attr.name) &&
- attr.name.name === 'plugins',
+ const pluginElement = makePluginElement(
+ pluginType,
+ importName,
+ displayName,
+ )
+
+ const pluginsProp = node.attributes.find(
+ (attr) =>
+ attr.type === 'JSXAttribute' &&
+ attr.name.type === 'JSXIdentifier' &&
+ attr.name.name === 'plugins',
+ )
+
+ if (
+ pluginsProp &&
+ pluginsProp.type === 'JSXAttribute' &&
+ pluginsProp.value &&
+ pluginsProp.value.type === 'JSXExpressionContainer' &&
+ pluginsProp.value.expression.type === 'ArrayExpression'
+ ) {
+ const expression = pluginsProp.value.expression
+
+ const alreadyExists = isPluginAlreadyInArray(
+ expression.elements,
+ pluginType,
+ importName,
+ displayName,
)
- // plugins found
- if (pluginsProp && t.isJSXAttribute(pluginsProp)) {
- // Check if plugins prop has a value
- if (
- pluginsProp.value &&
- t.isJSXExpressionContainer(pluginsProp.value)
- ) {
- const expression = pluginsProp.value.expression
-
- // If it's an array expression, add our plugin to it
- if (t.isArrayExpression(expression)) {
- // Check if plugin already exists
- const pluginExists = expression.elements.some((element) => {
- if (!element) return false
-
- // For function-based plugins, check if the function call exists
- if (pluginType === 'function') {
- return (
- t.isCallExpression(element) &&
- t.isIdentifier(element.callee) &&
- element.callee.name === importName
- )
- }
-
- // For JSX plugins, check object with name property
- if (!t.isObjectExpression(element)) return false
-
- return element.properties.some((prop) => {
- if (
- !t.isObjectProperty(prop) ||
- !t.isIdentifier(prop.key) ||
- prop.key.name !== 'name'
- ) {
- return false
- }
-
- return (
- t.isStringLiteral(prop.value) &&
- prop.value.value === displayName
- )
- })
- })
-
- if (!pluginExists) {
- // For function-based plugins, add them directly as function calls
- // For JSX plugins, wrap them in objects with name and render
- if (pluginType === 'function') {
- // Add directly: FormDevtoolsPlugin()
- expression.elements.push(
- t.callExpression(t.identifier(importName), []),
- )
- } else {
- // Add as object: { name: "...", render: }
- const renderValue = t.jsxElement(
- t.jsxOpeningElement(t.jsxIdentifier(importName), [], true),
- null,
- [],
- true,
- )
-
- expression.elements.push(
- t.objectExpression([
- t.objectProperty(
- t.identifier('name'),
- t.stringLiteral(displayName),
- ),
- t.objectProperty(t.identifier('render'), renderValue),
- ]),
- )
- }
-
- didTransform = true
- }
- }
- }
- } else {
- // No plugins prop exists, create one with our plugin
- // For function-based plugins, add them directly as function calls
- // For JSX plugins, wrap them in objects with name and render
- let pluginElement
- if (pluginType === 'function') {
- // Add directly: plugins={[FormDevtoolsPlugin()]}
- pluginElement = t.callExpression(t.identifier(importName), [])
- } else {
- // Add as object: plugins={[{ name: "...", render: }]}
- const renderValue = t.jsxElement(
- t.jsxOpeningElement(t.jsxIdentifier(importName), [], true),
- null,
- [],
- true,
- )
-
- pluginElement = t.objectExpression([
- t.objectProperty(
- t.identifier('name'),
- t.stringLiteral(displayName),
- ),
- t.objectProperty(t.identifier('render'), renderValue),
- ])
- }
-
- path.node.attributes.push(
- t.jsxAttribute(
- t.jsxIdentifier('plugins'),
- t.jsxExpressionContainer(t.arrayExpression([pluginElement])),
- ),
- )
-
- didTransform = true
+
+ if (alreadyExists) {
+ return
}
+
+ const insertionAt = expression.end - 1
+ const arraySource = code.slice(expression.start, expression.end)
+ const inner = arraySource.slice(1, -1).trim()
+
+ let separator = ''
+ if (inner.length > 0) {
+ separator = inner.endsWith(',') ? ' ' : ', '
+ }
+
+ edits.push({
+ at: insertionAt,
+ text: `${separator}${pluginElement}`,
+ })
+ return
}
- },
- })
- // Add import at the top of the file if transform happened
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
- if (didTransform) {
- const importDeclaration = t.importDeclaration(
- [t.importSpecifier(t.identifier(importName), t.identifier(importName))],
- t.stringLiteral(injection.packageName),
- )
+ if (!pluginsProp) {
+ const insertAt =
+ code[node.end - 2] === '/' ? node.end - 2 : node.end - 1
- // Find the last import declaration
- let lastImportIndex = -1
- ast.program.body.forEach((node, index) => {
- if (t.isImportDeclaration(node)) {
- lastImportIndex = index
+ edits.push({
+ at: insertAt,
+ text: ` plugins={[${pluginElement}]}`,
+ })
}
- })
+ },
+ }).visit(ast.program)
- // Insert after the last import or at the beginning
- ast.program.body.splice(lastImportIndex + 1, 0, importDeclaration)
+ if (edits.length === 0) {
+ return { didTransform: false, code }
}
- return didTransform
-}
+ if (!importExists) {
+ const imports = ast.program.body.filter(
+ (node) => node.type === 'ImportDeclaration',
+ )
+ const lastImport = imports[imports.length - 1]
+ const importAt = lastImport ? lastImport.end : 0
-/**
- * Detects if a file contains TanStack devtools import
- */
-export function detectDevtoolsFile(code: string): boolean {
- return detectDevtoolsImport(code)
+ const importText = `\nimport { ${importName} } from ${JSON.stringify(injection.packageName)};`
+ edits.push({ at: importAt, text: importText })
+ }
+
+ return {
+ didTransform: true,
+ code: applyEdits(code, edits),
+ }
}
/**
- * Injects a plugin into the TanStackDevtools component in a file
- * Reads the file, transforms it, and writes it back
+ * Injects a plugin into the TanStackDevtools component in a file.
+ * Reads the file, transforms it, and writes it back.
*/
export function injectPluginIntoFile(
filePath: string,
injection: PluginInjection,
): { success: boolean; error?: string } {
try {
- // Read the file
const code = readFileSync(filePath, 'utf-8')
+ const devtoolsComponentName = findDevtoolsComponentName(code)
- // Parse the code
- const ast = parse(code, {
- sourceType: 'module',
- plugins: ['jsx', 'typescript'],
- })
-
- // Find the devtools component name (handles renamed imports)
- const devtoolsComponentName = findDevtoolsComponentName(ast)
if (!devtoolsComponentName) {
return {
success: false,
@@ -309,27 +323,15 @@ export function injectPluginIntoFile(
}
}
- // Transform and inject
- const didTransform = transformAndInject(
- ast,
- injection,
- devtoolsComponentName,
- )
+ const result = transformAndInject(code, injection, devtoolsComponentName)
- if (!didTransform) {
+ if (!result.didTransform) {
return {
success: false,
error: 'Plugin already exists or no TanStackDevtools component found',
}
}
- // Generate the new code
- const result = gen(ast, {
- sourceMaps: false,
- retainLines: false,
- })
-
- // Write back to file
writeFileSync(filePath, result.code, 'utf-8')
return { success: true }
diff --git a/packages/devtools-vite/src/inject-source.test.ts b/packages/devtools-vite/src/inject-source.test.ts
index f2a344aa..2ddad9ef 100644
--- a/packages/devtools-vite/src/inject-source.test.ts
+++ b/packages/devtools-vite/src/inject-source.test.ts
@@ -55,8 +55,8 @@ describe('inject source', () => {
expect(output).toBe(
removeEmptySpace(`
export const Route = createFileRoute("/test")({
- component: function() { return Hello World
; }
- });
+ component: function() { return Hello World
},
+ })
`),
)
})
@@ -102,7 +102,7 @@ describe('inject source', () => {
addSourceToJsx(
`
export const Route = createFileRoute("/test")({
- component: function({...rest}) { return }
+ component: function({...rest}) { return }
})
`,
'test.jsx',
@@ -111,8 +111,8 @@ describe('inject source', () => {
expect(output).toBe(
removeEmptySpace(`
export const Route = createFileRoute("/test")({
- component: function({...rest}) { return ; }
- });
+ component: function({...rest}) { return }
+ })
`),
)
})
@@ -133,8 +133,8 @@ describe('inject source', () => {
expect(output).toBe(
removeEmptySpace(`
export const Route = createFileRoute("/test")({
- component: () => Hello World
- });
+ component: () => Hello World
,
+ })
`),
)
})
@@ -167,7 +167,7 @@ describe('inject source', () => {
const output = addSourceToJsx(
`
export const Route = createFileRoute("/test")({
- component: ({...rest}) => Hello World
,
+ component: ({...rest}) => Hello World
,
})
`,
'test.jsx',
@@ -180,7 +180,7 @@ describe('inject source', () => {
addSourceToJsx(
`
export const Route = createFileRoute("/test")({
- component: ({...rest}) => ,
+ component: ({...rest}) => ,
})
`,
'test.jsx',
@@ -189,8 +189,8 @@ describe('inject source', () => {
expect(output).toBe(
removeEmptySpace(`
export const Route = createFileRoute("/test")({
- component: ({...rest}) =>
- });
+ component: ({...rest}) => ,
+ })
`),
)
})
@@ -214,9 +214,9 @@ describe('inject source', () => {
removeEmptySpace(`
function Parent({ ...props }) {
function Child({ ...props }) {
- return
;
+ return
}
- return ;
+ return
}
`),
)
@@ -225,8 +225,8 @@ describe('inject source', () => {
const output = removeEmptySpace(
addSourceToJsx(
`
-
- import Custom from "external";
+
+ import Custom from "external"
function test({...props }) {
return
@@ -237,10 +237,10 @@ function test({...props }) {
)
expect(output).toBe(
removeEmptySpace(`
- import Custom from "external";
+ import Custom from "external"
function test({...props }) {
- return ;
+ return
}`),
)
})
@@ -258,7 +258,7 @@ function test({...props }) {
expect(output).toBe(
removeEmptySpace(`
function test(props) {
- return ;
+ return
}
`),
)
@@ -292,9 +292,9 @@ function test(props) {
expect(output).toBe(
removeEmptySpace(`
function test(props) {
- return )
}
`),
)
@@ -316,9 +316,9 @@ function test(props) {
expect(output).toBe(
removeEmptySpace(`
function test({...props}) {
- return )
}
`),
)
@@ -340,9 +340,9 @@ function test({...props}) {
expect(output).toBe(
removeEmptySpace(`
function test({...rest}) {
- return )
}
`),
)
@@ -374,7 +374,7 @@ function test({...rest}) {
expect(output).toBe(
removeEmptySpace(`
function test({ ...props }) {
- return ;
+ return
}
`),
)
@@ -406,7 +406,7 @@ function test({...rest}) {
expect(output).toBe(
removeEmptySpace(`
function test({ ...props }) {
- return ;
+ return
}
`),
)
@@ -438,7 +438,7 @@ function test({...rest}) {
expect(output).toBe(
removeEmptySpace(`
function test({ ...props }) {
- return ;
+ return
}
`),
)
@@ -459,8 +459,8 @@ function test({...rest}) {
expect(output).toBe(
removeEmptySpace(`
const ButtonWithProps = function test(props) {
- return ;
- };
+ return
+ }
`),
)
})
@@ -493,10 +493,10 @@ function test({...rest}) {
expect(output).toBe(
removeEmptySpace(`
const ButtonWithProps = function test(props) {
- return )
+ }
`),
)
})
@@ -517,10 +517,10 @@ function test({...rest}) {
expect(output).toBe(
removeEmptySpace(`
const ButtonWithProps = function test({...props}) {
- return )
+ }
`),
)
})
@@ -541,10 +541,10 @@ function test({...rest}) {
expect(output).toBe(
removeEmptySpace(`
const ButtonWithProps = function test({...rest}) {
- return )
+ }
`),
)
})
@@ -575,8 +575,8 @@ function test({...rest}) {
expect(output).toBe(
removeEmptySpace(`
const ButtonWithProps = function test({ ...props }) {
- return ;
- };
+ return
+ }
`),
)
})
@@ -607,8 +607,8 @@ function test({...rest}) {
expect(output).toBe(
removeEmptySpace(`
const ButtonWithProps = function test({ ...props }) {
- return ;
- };
+ return
+ }
`),
)
})
@@ -639,8 +639,8 @@ function test({...rest}) {
expect(output).toBe(
removeEmptySpace(`
export const ButtonWithProps = function test({ ...props }) {
- return ;
- };
+ return
+ }
`),
)
})
@@ -660,8 +660,8 @@ function test({...rest}) {
expect(output).toBe(
removeEmptySpace(`
const ButtonWithProps = (props) => {
- return ;
- };
+ return
+ }
`),
)
})
@@ -694,10 +694,10 @@ function test({...rest}) {
expect(output).toBe(
removeEmptySpace(`
const ButtonWithProps = (props) => {
- return )
+ }
`),
)
})
@@ -718,10 +718,10 @@ function test({...rest}) {
expect(output).toBe(
removeEmptySpace(`
const ButtonWithProps = ({...props}) => {
- return )
+ }
`),
)
})
@@ -742,10 +742,10 @@ function test({...rest}) {
expect(output).toBe(
removeEmptySpace(`
const ButtonWithProps = ({...rest}) => {
- return )
+ }
`),
)
})
@@ -764,8 +764,8 @@ function test({...rest}) {
expect(output).toBe(
removeEmptySpace(`
const ButtonWithProps = ({ children, ...rest }) => {
- return ;
- };
+ return
+ }
`),
)
})
@@ -784,8 +784,8 @@ function test({...rest}) {
expect(output).toBe(
removeEmptySpace(`
const ButtonWithProps = ({ ...props }) => {
- return ;
- };
+ return
+ }
`),
)
})
@@ -816,8 +816,8 @@ function test({...rest}) {
expect(output).toBe(
removeEmptySpace(`
const ButtonWithProps = ({ ...props }) => {
- return ;
- };
+ return
+ }
`),
)
})
@@ -848,8 +848,8 @@ function test({...rest}) {
expect(output).toBe(
removeEmptySpace(`
export const ButtonWithProps = ({ ...props }) => {
- return ;
- };
+ return
+ }
`),
)
})
diff --git a/packages/devtools-vite/src/inject-source.ts b/packages/devtools-vite/src/inject-source.ts
index 28502e3b..5c0aa359 100644
--- a/packages/devtools-vite/src/inject-source.ts
+++ b/packages/devtools-vite/src/inject-source.ts
@@ -1,283 +1,210 @@
import { normalizePath } from 'vite'
-import { gen, parse, t, trav } from './babel'
+import { parseSync, visitorKeys } from 'oxc-parser'
import { matcher } from './matcher'
-import type { types as Babel, NodePath } from '@babel/core'
-import type { ParseResult } from '@babel/parser'
-
-const getPropsNameFromFunctionDeclaration = (
- functionDeclaration:
- | t.VariableDeclarator
- | t.FunctionExpression
- | t.FunctionDeclaration
- | t.ArrowFunctionExpression,
-) => {
- let propsName: string | null = null
-
- if (functionDeclaration.type === 'FunctionExpression') {
- const firstArgument = functionDeclaration.params[0]
- // handles (props) => {}
- if (firstArgument && firstArgument.type === 'Identifier') {
- propsName = firstArgument.name
- }
- // handles ({ ...props }) => {}
- if (firstArgument && firstArgument.type === 'ObjectPattern') {
- firstArgument.properties.forEach((prop) => {
- if (
- prop.type === 'RestElement' &&
- prop.argument.type === 'Identifier'
- ) {
- propsName = prop.argument.name
- }
- })
+
+type IgnoreOptions = {
+ files?: Array
+ components?: Array
+}
+
+type Edit = {
+ 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 propsName
}
- if (functionDeclaration.type === 'ArrowFunctionExpression') {
- const firstArgument = functionDeclaration.params[0]
- // handles (props) => {}
- if (firstArgument && firstArgument.type === 'Identifier') {
- propsName = firstArgument.name
- }
- // handles ({ ...props }) => {}
- if (firstArgument && firstArgument.type === 'ObjectPattern') {
- firstArgument.properties.forEach((prop) => {
- if (
- prop.type === 'RestElement' &&
- prop.argument.type === 'Identifier'
- ) {
- propsName = prop.argument.name
- }
- })
+ return starts
+}
+
+const offsetToLineColumn = (offset: number, lineStarts: Array) => {
+ let low = 0
+ let high = lineStarts.length - 1
+
+ while (low <= high) {
+ const mid = (low + high) >> 1
+ const lineStart = lineStarts[mid] ?? 0
+ if (lineStart <= offset) {
+ low = mid + 1
+ } else {
+ high = mid - 1
}
- return propsName
}
- if (functionDeclaration.type === 'FunctionDeclaration') {
- const firstArgument = functionDeclaration.params[0]
- // handles (props) => {}
- if (firstArgument && firstArgument.type === 'Identifier') {
- propsName = firstArgument.name
- }
- // handles ({ ...props }) => {}
- if (firstArgument && firstArgument.type === 'ObjectPattern') {
- firstArgument.properties.forEach((prop) => {
- if (
- prop.type === 'RestElement' &&
- prop.argument.type === 'Identifier'
- ) {
- propsName = prop.argument.name
- }
- })
- }
- return propsName
+
+ const lineIndex = Math.max(0, high)
+ const lineStart = lineStarts[lineIndex] ?? 0
+
+ return {
+ line: lineIndex + 1,
+ column: offset - lineStart + 1,
}
- // Arrow function case
- if (
- functionDeclaration.init?.type === 'ArrowFunctionExpression' ||
- functionDeclaration.init?.type === 'FunctionExpression'
- ) {
- const firstArgument = functionDeclaration.init.params[0]
- // handles (props) => {}
- if (firstArgument && firstArgument.type === 'Identifier') {
- propsName = firstArgument.name
- }
- // handles ({ ...props }) => {}
- if (firstArgument && firstArgument.type === 'ObjectPattern') {
- firstArgument.properties.forEach((prop) => {
- if (
- prop.type === 'RestElement' &&
- prop.argument.type === 'Identifier'
- ) {
- propsName = prop.argument.name
- }
- })
+}
+
+const getPropsNameFromParams = (params: Array): string | null => {
+ const firstArgument = params[0]
+ if (!firstArgument) {
+ return null
+ }
+
+ if (firstArgument.type === 'Identifier') {
+ return firstArgument.name
+ }
+
+ if (firstArgument.type === 'ObjectPattern') {
+ for (const prop of firstArgument.properties) {
+ if (prop.type === 'RestElement' && prop.argument.type === 'Identifier') {
+ return prop.argument.name
+ }
}
}
- return propsName
+
+ return null
}
-const getNameOfElement = (
- element: t.JSXIdentifier | t.JSXMemberExpression | t.JSXNamespacedName,
-): string => {
+const getElementName = (element: any): string => {
if (element.type === 'JSXIdentifier') {
return element.name
}
+
if (element.type === 'JSXMemberExpression') {
- return `${getNameOfElement(element.object)}.${getNameOfElement(element.property)}`
+ return `${getElementName(element.object)}.${getElementName(element.property)}`
+ }
+
+ if (element.type === 'JSXNamespacedName') {
+ return `${element.namespace.name}:${element.name.name}`
}
- return `${element.namespace.name}:${element.name.name}`
+ return ''
}
-const transformJSX = (
- element: NodePath,
- propsName: string | null,
- file: string,
- ignorePatterns: Array,
-) => {
- const loc = element.node.loc
- if (!loc) return
- const line = loc.start.line
- const column = loc.start.column
- const nameOfElement = getNameOfElement(element.node.name)
- const isIgnored = matcher(ignorePatterns, nameOfElement)
- if (
- nameOfElement === 'Fragment' ||
- nameOfElement === 'React.Fragment' ||
- isIgnored
- ) {
- return
- }
- const hasDataSource = element.node.attributes.some(
+const hasDataSourceAttribute = (attributes: Array) => {
+ return attributes.some(
(attr) =>
attr.type === 'JSXAttribute' &&
attr.name.type === 'JSXIdentifier' &&
attr.name.name === 'data-tsd-source',
)
- // Check if props are spread
- const hasSpread = element.node.attributes.some(
+}
+
+const hasPropsSpread = (attributes: Array, propsName: string | null) => {
+ if (!propsName) {
+ return false
+ }
+
+ return attributes.some(
(attr) =>
attr.type === 'JSXSpreadAttribute' &&
attr.argument.type === 'Identifier' &&
attr.argument.name === propsName,
)
-
- if (hasSpread || hasDataSource) {
- // Do not inject if props are spread
- return
- }
-
- // Inject data-source as a string: "::"
- element.node.attributes.push(
- t.jsxAttribute(
- t.jsxIdentifier('data-tsd-source'),
- t.stringLiteral(`${file}:${line}:${column + 1}`),
- ),
- )
-
- return true
}
-const transform = (
- ast: ParseResult,
- file: string,
- ignorePatterns: Array,
-) => {
- let didTransform = false
-
- trav(ast, {
- FunctionDeclaration(functionDeclaration) {
- const propsName = getPropsNameFromFunctionDeclaration(
- functionDeclaration.node,
- )
- functionDeclaration.traverse({
- JSXOpeningElement(element) {
- const transformed = transformJSX(
- element,
- propsName,
- file,
- ignorePatterns,
- )
- if (transformed) {
- didTransform = true
- }
- },
- })
- },
- ArrowFunctionExpression(path) {
- const propsName = getPropsNameFromFunctionDeclaration(path.node)
- path.traverse({
- JSXOpeningElement(element) {
- const transformed = transformJSX(
- element,
- propsName,
- file,
- ignorePatterns,
- )
- if (transformed) {
- didTransform = true
- }
- },
- })
- },
- FunctionExpression(path) {
- const propsName = getPropsNameFromFunctionDeclaration(path.node)
- path.traverse({
- JSXOpeningElement(element) {
- const transformed = transformJSX(
- element,
- propsName,
- file,
- ignorePatterns,
- )
- if (transformed) {
- didTransform = true
- }
- },
- })
- },
- VariableDeclaration(path) {
- const functionDeclaration = path.node.declarations.find((decl) => {
- return (
- decl.init?.type === 'ArrowFunctionExpression' ||
- decl.init?.type === 'FunctionExpression'
- )
- })
- if (!functionDeclaration) {
- return
- }
- const propsName = getPropsNameFromFunctionDeclaration(functionDeclaration)
-
- path.traverse({
- JSXOpeningElement(element) {
- const transformed = transformJSX(
- element,
- propsName,
- file,
- ignorePatterns,
- )
- if (transformed) {
- didTransform = true
- }
- },
- })
- },
- })
+const applyEdits = (code: string, edits: Array) => {
+ const ordered = [...edits].sort((a, b) => b.at - a.at)
+ let next = code
+
+ for (const edit of ordered) {
+ next = next.slice(0, edit.at) + edit.text + next.slice(edit.at)
+ }
- return didTransform
+ return next
}
export function addSourceToJsx(
code: string,
id: string,
- ignore: {
- files?: Array
- components?: Array
- } = {},
+ ignore: IgnoreOptions = {},
) {
const [filePath] = id.split('?')
// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
const location = filePath?.replace(normalizePath(process.cwd()), '')!
- const fileIgnored = matcher(ignore.files || [], location)
- if (fileIgnored) {
+ if (matcher(ignore.files || [], location)) {
return
}
+
try {
- const ast = parse(code, {
+ const result = parseSync(filePath ?? id, code, {
sourceType: 'module',
- plugins: ['jsx', 'typescript'],
+ lang: 'tsx',
+ range: true,
+ preserveParens: true,
})
- const didTransform = transform(ast, location, ignore.components || [])
- if (!didTransform) {
+
+ if (result.errors.length > 0) {
return
}
- return gen(ast, {
- sourceMaps: true,
- retainLines: true,
- filename: id,
- sourceFileName: filePath,
- })
- } catch (e) {
+
+ const edits: Array = []
+ const lineStarts = buildLineStarts(code)
+
+ const walk = (node: any, propsName: string | null) => {
+ if (!node || typeof node !== 'object' || typeof node.type !== 'string') {
+ return
+ }
+
+ let scopedPropsName = propsName
+ if (
+ node.type === 'FunctionDeclaration' ||
+ node.type === 'FunctionExpression' ||
+ node.type === 'ArrowFunctionExpression'
+ ) {
+ scopedPropsName = getPropsNameFromParams(node.params || [])
+ }
+
+ if (node.type === 'JSXOpeningElement') {
+ const nameOfElement = getElementName(node.name)
+ const isIgnored = matcher(ignore.components || [], nameOfElement)
+
+ if (
+ nameOfElement !== 'Fragment' &&
+ nameOfElement !== 'React.Fragment' &&
+ !isIgnored &&
+ !hasDataSourceAttribute(node.attributes || []) &&
+ !hasPropsSpread(node.attributes || [], scopedPropsName)
+ ) {
+ const { line, column } = offsetToLineColumn(node.start, lineStarts)
+ const attributeText = ` data-tsd-source="${location}:${line}:${column}"`
+
+ const closeAngle = node.end - 1
+ const insertAt = code[node.end - 2] === '/' ? node.end - 2 : closeAngle
+
+ edits.push({
+ at: insertAt,
+ text: attributeText,
+ })
+ }
+ }
+
+ const keys = visitorKeys[node.type] || []
+ for (const key of keys) {
+ const child = node[key]
+ if (Array.isArray(child)) {
+ for (const item of child) {
+ walk(item, scopedPropsName)
+ }
+ } else {
+ walk(child, scopedPropsName)
+ }
+ }
+ }
+
+ walk(result.program, null)
+
+ if (edits.length === 0) {
+ return
+ }
+
+ return {
+ code: applyEdits(code, edits),
+ map: null,
+ }
+ } catch {
return
}
}
diff --git a/packages/devtools-vite/src/remove-devtools.test.ts b/packages/devtools-vite/src/remove-devtools.test.ts
index 5fdee0e2..f04b8feb 100644
--- a/packages/devtools-vite/src/remove-devtools.test.ts
+++ b/packages/devtools-vite/src/remove-devtools.test.ts
@@ -21,9 +21,9 @@ import {
createRouter,
} from '@tanstack/react-router'
import { TanStackDevtools } from '@tanstack/react-devtools'
-
-
+
+
export default function DevtoolsExample() {
return (
<>
@@ -63,15 +63,15 @@ export default function DevtoolsExample() {
RouterProvider,
createRootRoute,
createRoute,
- createRouter
- } from '@tanstack/react-router';
+ createRouter,
+ } from '@tanstack/react-router'
+
-
export default function DevtoolsExample() {
- return (<>
+ return (<>
- >);
-
+ >)
+
}
`),
@@ -93,9 +93,9 @@ import {
createRouter,
} from '@tanstack/react-router'
import { TanStackDevtools as Devtools } from '@tanstack/react-devtools'
-
-
+
+
export default function DevtoolsExample() {
return (
<>
@@ -135,15 +135,15 @@ export default function DevtoolsExample() {
RouterProvider,
createRootRoute,
createRoute,
- createRouter
- } from '@tanstack/react-router';
+ createRouter,
+ } from '@tanstack/react-router'
+
-
export default function DevtoolsExample() {
- return ( <>
+ return ( <>
- >);
-
+ >)
+
}
`),
@@ -165,9 +165,9 @@ import {
createRouter,
} from '@tanstack/react-router'
import * as Tools from '@tanstack/react-devtools'
-
-
+
+
export default function DevtoolsExample() {
return (
<>
@@ -207,15 +207,15 @@ export default function DevtoolsExample() {
RouterProvider,
createRootRoute,
createRoute,
- createRouter
- } from '@tanstack/react-router';
+ createRouter,
+ } from '@tanstack/react-router'
+
-
export default function DevtoolsExample() {
- return (<>
+ return (<>
- >);
-
+ >)
+
}
`),
@@ -225,7 +225,7 @@ export default function DevtoolsExample() {
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 {
@@ -237,9 +237,9 @@ import {
createRouter,
} from '@tanstack/react-router'
import * as Tools from '@tanstack/react-devtools'
-
-
+
+
export default function DevtoolsExample() {
return (
<>
@@ -266,30 +266,31 @@ export default function DevtoolsExample() {
>
)
-}`,
+}
+`,
'test.jsx',
)!.code,
)
expect(output).toBe(
- removeEmptySpace(`
+ removeEmptySpace(`
import {
Link,
Outlet,
RouterProvider,
createRootRoute,
createRoute,
- createRouter
-} from '@tanstack/react-router' ;
-
+ createRouter,
+} from '@tanstack/react-router'
+
+
-
export default function DevtoolsExample() {
return (
- <>
+ <>
>
- );
+ )
}
`),
)
@@ -299,13 +300,13 @@ export default function DevtoolsExample() {
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 (
<>
@@ -317,27 +318,28 @@ export default function DevtoolsExample() {
{
name: 'TanStack Query',
render: ,
- }
+ }
]}
/>
>
)
-}`,
+}
+`,
'test.jsx',
)!.code,
)
expect(output).toBe(
- removeEmptySpace(`
+ removeEmptySpace(`
import { test } from '@tanstack/react-query-devtools';
-
+
export default function DevtoolsExample() {
return (
- <>
+ <>
>
- );
+ )
}
`),
)
@@ -346,13 +348,13 @@ export default function DevtoolsExample() {
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 (
<>
@@ -364,27 +366,28 @@ export default function DevtoolsExample() {
{
name: 'TanStack Query',
render: ,
- }
+ }
]}
/>
>
)
-}`,
+}
+`,
'test.jsx',
)!.code,
)
expect(output).toBe(
- removeEmptySpace(`
- import * as Stuff from '@tanstack/react-query-devtools';
-
+ removeEmptySpace(`
+ import * as Stuff from '@tanstack/react-query-devtools'
+
export default function DevtoolsExample() {
return (
- <>
+ <>
>
- );
+ )
}
`),
)
@@ -393,10 +396,10 @@ export default function DevtoolsExample() {
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'
-
+ `
+ import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools'
+import * as Tools from '@tanstack/react-devtools'
+
export default function DevtoolsExample() {
return (
<>
@@ -408,25 +411,26 @@ export default function DevtoolsExample() {
{
name: 'TanStack Query',
render: ,
- }
+ }
]}
/>
>
)
-}`,
+}
+`,
'test.jsx',
)!.code,
)
expect(output).toBe(
- removeEmptySpace(`
+ removeEmptySpace(`
export default function DevtoolsExample() {
return (
- <>
+ <>
>
- );
+ )
}
`),
)
@@ -435,10 +439,10 @@ export default function DevtoolsExample() {
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'
-
+ `
+ import { plugin } from '@tanstack/react-query-devtools'
+import * as Tools from '@tanstack/react-devtools'
+
export default function DevtoolsExample() {
return (
<>
@@ -450,25 +454,26 @@ export default function DevtoolsExample() {
{
name: 'TanStack Query',
render: plugin()
- }
+ }
]}
/>
>
)
-}`,
+}
+`,
'test.jsx',
)!.code,
)
expect(output).toBe(
- removeEmptySpace(`
+ removeEmptySpace(`
export default function DevtoolsExample() {
return (
- <>
+ <>
>
- );
+ )
}
`),
)
@@ -477,10 +482,10 @@ export default function DevtoolsExample() {
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'
-
+ `
+ import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools'
+import * as Tools from '@tanstack/react-devtools'
+
export default function DevtoolsExample() {
return (
<>
@@ -492,25 +497,26 @@ export default function DevtoolsExample() {
{
name: 'TanStack Query',
render: () =>
- }
+ }
]}
/>
>
)
-}`,
+}
+`,
'test.jsx',
)!.code,
)
expect(output).toBe(
- removeEmptySpace(`
+ removeEmptySpace(`
export default function DevtoolsExample() {
return (
- <>
+ <>
>
- );
+ )
}
`),
)
@@ -519,10 +525,10 @@ export default function DevtoolsExample() {
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'
-
+ `
+ import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools'
+import * as Tools from '@tanstack/react-devtools'
+
export default function DevtoolsExample() {
return (
<>
@@ -534,25 +540,26 @@ export default function DevtoolsExample() {
{
name: 'TanStack Query',
render: ReactQueryDevtoolsPanel
- }
+ }
]}
/>
>
)
-}`,
+}
+`,
'test.jsx',
)!.code,
)
expect(output).toBe(
- removeEmptySpace(`
+ 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 6171f47c..58a3d95e 100644
--- a/packages/devtools-vite/src/remove-devtools.ts
+++ b/packages/devtools-vite/src/remove-devtools.ts
@@ -1,194 +1,325 @@
-import { gen, parse, trav } from './babel'
-import type { t } from './babel'
-import type { types as Babel, NodePath } from '@babel/core'
-import type { ParseResult } from '@babel/parser'
+import { parseSync, visitorKeys } from 'oxc-parser'
+
+type Edit = {
+ start: number
+ end: number
+ text: string
+}
+
+type ImportDeclarationNode = {
+ type: 'ImportDeclaration'
+ source: { value: string }
+ specifiers: Array
+ start: number
+ end: number
+}
const isTanStackDevtoolsImport = (source: string) =>
source === '@tanstack/react-devtools' ||
source === '@tanstack/devtools' ||
source === '@tanstack/solid-devtools'
-const getImportedNames = (importDecl: t.ImportDeclaration) => {
- return importDecl.specifiers.map((spec) => spec.local.name)
+const isImportDeclarationNode = (node: any) => {
+ return (
+ node?.type === 'ImportDeclaration' &&
+ typeof node?.source?.value === 'string' &&
+ Array.isArray(node?.specifiers)
+ )
+}
+
+const getImportedNames = (importDecl: ImportDeclarationNode): Array => {
+ return importDecl.specifiers
+ .map((spec: any) => spec.local?.name)
+ .filter((name: unknown): name is string => typeof name === 'string')
+}
+
+const getJsxElementName = (node: any): string | null => {
+ if (node.type === 'JSXIdentifier') {
+ return node.name
+ }
+
+ return null
}
-const getLeftoverImports = (node: NodePath) => {
+const getLeftoverImports = (jsxElement: any) => {
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)
+
+ const openingElement = jsxElement.openingElement
+ if (!openingElement || !Array.isArray(openingElement.attributes)) {
+ return finalReferences
+ }
+
+ for (const attribute of openingElement.attributes) {
+ if (attribute.type !== 'JSXAttribute') {
+ continue
+ }
+
+ if (attribute.name.type !== 'JSXIdentifier' || attribute.name.name !== 'plugins') {
+ continue
+ }
+
+ if (
+ !attribute.value ||
+ attribute.value.type !== 'JSXExpressionContainer' ||
+ attribute.value.expression.type !== 'ArrayExpression'
+ ) {
+ continue
+ }
+
+ for (const element of attribute.value.expression.elements) {
+ if (!element || element.type !== 'ObjectExpression') {
+ continue
+ }
+
+ for (const prop of element.properties) {
+ if (
+ prop.type !== 'Property' ||
+ prop.key.type !== 'Identifier' ||
+ prop.key.name !== 'render'
+ ) {
+ continue
+ }
+
+ const value = prop.value
+
+ if (value.type === 'JSXElement') {
+ const elementName = getJsxElementName(value.openingElement?.name)
+ if (elementName) {
+ finalReferences.push(elementName)
}
- })
+ continue
+ }
+
+ if (value.type === 'ArrowFunctionExpression' || value.type === 'FunctionExpression') {
+ const body = value.body
+ if (body?.type === 'JSXElement') {
+ const elementName = getJsxElementName(body.openingElement?.name)
+ if (elementName) {
+ finalReferences.push(elementName)
+ }
+ }
+ continue
+ }
+
+ if (value.type === 'Identifier') {
+ finalReferences.push(value.name)
+ continue
+ }
+
+ if (value.type === 'CallExpression' && value.callee.type === 'Identifier') {
+ finalReferences.push(value.callee.name)
+ }
}
- },
- })
+ }
+ }
+
return finalReferences
}
-const transform = (ast: ParseResult) => {
+const applyEdits = (code: string, edits: Array) => {
+ const ordered = [...edits].sort((a, b) => b.start - a.start)
+ let next = code
+
+ for (const edit of ordered) {
+ next = next.slice(0, edit.start) + edit.text + next.slice(edit.end)
+ }
+
+ return next
+}
+
+const buildImportText = (node: any, keptSpecifiers: Array) => {
+ const source = node.source.value
+
+ const defaultSpecifier = keptSpecifiers.find(
+ (spec) => spec.type === 'ImportDefaultSpecifier',
+ )
+ const namespaceSpecifier = keptSpecifiers.find(
+ (spec) => spec.type === 'ImportNamespaceSpecifier',
+ )
+ const namedSpecifiers = keptSpecifiers.filter(
+ (spec) => spec.type === 'ImportSpecifier',
+ )
+
+ const parts: Array = []
+
+ if (defaultSpecifier) {
+ parts.push(defaultSpecifier.local.name)
+ }
+
+ if (namespaceSpecifier) {
+ parts.push(`* as ${namespaceSpecifier.local.name}`)
+ }
+
+ if (namedSpecifiers.length > 0) {
+ const names = namedSpecifiers.map((spec) => {
+ const importedName =
+ spec.imported?.type === 'Identifier' ? spec.imported.name : spec.local.name
+ const localName = spec.local.name
+
+ if (importedName === localName) {
+ return importedName
+ }
+
+ return `${importedName} as ${localName}`
+ })
+
+ parts.push(`{ ${names.join(', ')} }`)
+ }
+
+ if (parts.length === 0) {
+ return ''
+ }
+
+ return `import ${parts.join(', ')} from '${source}';`
+}
+
+const getOpeningTagRootName = (name: any): string | null => {
+ if (name.type === 'JSXIdentifier') {
+ return name.name
+ }
+
+ if (name.type === 'JSXMemberExpression' && name.object.type === 'JSXIdentifier') {
+ return name.object.name
+ }
+
+ return null
+}
+
+const transform = (code: string) => {
+ const ast = parseSync('remove-devtools.tsx', code, {
+ sourceType: 'module',
+ lang: 'tsx',
+ range: true,
+ preserveParens: true,
+ })
+
+ if (ast.errors.length > 0) {
+ return { didTransform: false, code }
+ }
+
let didTransform = false
- const devtoolsComponentNames = new Set()
+ const edits: Array = []
const finalReferences: Array = []
+ const devtoolsComponentNames = new Set()
- const transformations: Array<() => void> = []
+ const importDeclarations = ast.program.body.filter((node: any) =>
+ isImportDeclarationNode(node),
+ ) as Array
- trav(ast, {
- ImportDeclaration(path) {
- const importSource = path.node.source.value
- if (isTanStackDevtoolsImport(importSource)) {
- getImportedNames(path.node).forEach((name) =>
- devtoolsComponentNames.add(name),
- )
+ for (const importDecl of importDeclarations) {
+ const importSource = importDecl.source.value
+ if (!isTanStackDevtoolsImport(importSource)) {
+ continue
+ }
- transformations.push(() => {
- path.remove()
- })
+ getImportedNames(importDecl).forEach((name: string) =>
+ devtoolsComponentNames.add(name),
+ )
+ edits.push({
+ start: importDecl.start,
+ end: importDecl.end,
+ text: '',
+ })
+ didTransform = true
+ }
- didTransform = true
- }
- },
- JSXElement(path) {
- const opening = path.node.openingElement
- if (
- opening.name.type === 'JSXIdentifier' &&
- devtoolsComponentNames.has(opening.name.name)
- ) {
- 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)
- ) {
- const refs = getLeftoverImports(path)
- finalReferences.push(...refs)
- transformations.push(() => {
- path.remove()
+ const walk = (node: any) => {
+ if (!node || typeof node !== 'object' || typeof node.type !== 'string') {
+ return
+ }
+
+ if (node.type === 'JSXElement') {
+ const rootName = getOpeningTagRootName(node.openingElement?.name)
+
+ if (rootName && devtoolsComponentNames.has(rootName)) {
+ finalReferences.push(...getLeftoverImports(node))
+ edits.push({
+ start: node.start,
+ end: node.end,
+ text: '',
})
didTransform = true
+ return
}
- },
- })
+ }
- 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()
- }
- })
- }
+ const keys = visitorKeys[node.type] || []
+ for (const key of keys) {
+ const child = node[key]
+ if (Array.isArray(child)) {
+ for (const item of child) {
+ walk(item)
}
+ } else {
+ walk(child)
}
- },
- })
+ }
+ }
- transformations.forEach((fn) => fn())
+ walk(ast.program)
- return didTransform
-}
+ if (finalReferences.length > 0) {
+ const finalReferenceSet = new Set(finalReferences)
-export function removeDevtools(code: string, id: string) {
- const [filePath] = id.split('?')
+ for (const importDecl of importDeclarations) {
+ if (isTanStackDevtoolsImport(importDecl.source.value)) {
+ continue
+ }
+
+ const importSpecifiers = importDecl.specifiers
+ const removableImportSpecifiers = importSpecifiers.filter(
+ (specifier: any) =>
+ specifier.type === 'ImportSpecifier' &&
+ finalReferenceSet.has(specifier.local.name),
+ )
+
+ if (removableImportSpecifiers.length === 0) {
+ continue
+ }
+ const keptSpecifiers = importSpecifiers.filter(
+ (specifier: any) => !removableImportSpecifiers.includes(specifier),
+ )
+
+ if (keptSpecifiers.length === 0) {
+ edits.push({
+ start: importDecl.start,
+ end: importDecl.end,
+ text: '',
+ })
+ continue
+ }
+
+ edits.push({
+ start: importDecl.start,
+ end: importDecl.end,
+ text: buildImportText(importDecl, keptSpecifiers),
+ })
+ }
+ }
+
+ if (!didTransform) {
+ return { didTransform: false, code }
+ }
+
+ return {
+ didTransform: true,
+ code: applyEdits(code, edits),
+ }
+}
+
+export function removeDevtools(code: string, _id: string) {
try {
- const ast = parse(code, {
- sourceType: 'module',
- plugins: ['jsx', 'typescript'],
- })
- const didTransform = transform(ast)
- if (!didTransform) {
+ const result = transform(code)
+ if (!result.didTransform) {
return
}
- return gen(ast, {
- sourceMaps: true,
- retainLines: true,
- filename: id,
- sourceFileName: filePath,
- })
- } catch (e) {
+
+ return {
+ code: result.code,
+ map: null,
+ }
+ } catch {
return
}
}
diff --git a/packages/devtools/skills/devtools-marketplace/SKILL.md b/packages/devtools/skills/devtools-marketplace/SKILL.md
index 50a55000..00523939 100644
--- a/packages/devtools/skills/devtools-marketplace/SKILL.md
+++ b/packages/devtools/skills/devtools-marketplace/SKILL.md
@@ -222,7 +222,7 @@ The auto-install pipeline lives in `packages/devtools-vite/src/inject-plugin.ts`
1. **Package installation** -- The Vite plugin detects the project's package manager and runs the appropriate install command.
2. **File detection** -- It scans project files for imports from `@tanstack/react-devtools`, `@tanstack/solid-devtools`, `@tanstack/vue-devtools`, etc.
-3. **AST transformation** -- It parses the file with Babel, finds the ` ` JSX element, and modifies the `plugins` prop.
+3. **AST transformation** -- It parses the file with Oxc, finds the ` ` JSX element, and modifies the `plugins` prop.
4. **Import insertion** -- It adds `import { } from ''` after the last existing import.
5. **Plugin injection** -- Based on `pluginImport.type`:
- `'function'`: Appends `ImportName()` directly to the plugins array
diff --git a/packages/devtools/skills/devtools-production/SKILL.md b/packages/devtools/skills/devtools-production/SKILL.md
index 43f6e3c6..3f962a66 100644
--- a/packages/devtools/skills/devtools-production/SKILL.md
+++ b/packages/devtools/skills/devtools-production/SKILL.md
@@ -31,7 +31,7 @@ TanStack Devtools has two independent mechanisms for keeping devtools out of pro
The `@tanstack/devtools-vite` plugin includes a sub-plugin named `@tanstack/devtools:remove-devtools-on-build`. When `removeDevtoolsOnBuild` is `true` (the default), this plugin runs during `vite build` and any non-`serve` command where the mode is `production`.
-It uses Babel to parse every source file, find imports from these packages, and remove them along with any JSX elements they produce:
+It uses Oxc to parse every source file, find imports from these packages, and remove them along with any JSX elements they produce:
- `@tanstack/react-devtools`
- `@tanstack/preact-devtools`
@@ -452,7 +452,7 @@ For staging/preview environments where you want devtools but not in the final pr
## Key Source Files
- `packages/devtools-vite/src/plugin.ts` -- Vite plugin entry, `removeDevtoolsOnBuild` option, sub-plugin registration
-- `packages/devtools-vite/src/remove-devtools.ts` -- AST-based stripping logic (Babel parse, traverse, codegen)
+- `packages/devtools-vite/src/remove-devtools.ts` -- AST-based stripping logic (Oxc)
- `packages/devtools/package.json` -- Conditional exports (`browser.development` -> `dev.js`, `browser` -> `index.js`, `node`/`workerd` -> `server.js`)
- `packages/devtools/tsup.config.ts` -- Build config producing `dev.js`, `index.js`, `server.js` via `tsup-preset-solid`
- `packages/devtools-utils/src/react/plugin.tsx` -- `createReactPlugin` returning `[Plugin, NoOpPlugin]`
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 58adb63a..724889d5 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -910,21 +910,6 @@ importers:
packages/devtools-vite:
dependencies:
- '@babel/core':
- specifier: ^7.28.4
- version: 7.28.6
- '@babel/generator':
- specifier: ^7.28.3
- version: 7.28.6
- '@babel/parser':
- specifier: ^7.28.4
- version: 7.28.6
- '@babel/traverse':
- specifier: ^7.28.4
- version: 7.28.6
- '@babel/types':
- specifier: ^7.28.4
- version: 7.28.6
'@tanstack/devtools-client':
specifier: workspace:*
version: link:../devtools-client
@@ -937,6 +922,9 @@ importers:
launch-editor:
specifier: ^2.11.1
version: 2.12.0
+ oxc-parser:
+ specifier: ^0.121.0
+ version: 0.121.0
picomatch:
specifier: ^4.0.3
version: 4.0.3
@@ -944,15 +932,6 @@ importers:
specifier: ^6.0.0 || ^7.0.0 || ^8.0.0
version: 7.3.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
devDependencies:
- '@types/babel__core':
- specifier: ^7.20.5
- version: 7.20.5
- '@types/babel__generator':
- specifier: ^7.27.0
- version: 7.27.0
- '@types/babel__traverse':
- specifier: ^7.28.0
- version: 7.28.0
'@types/picomatch':
specifier: ^4.0.2
version: 4.0.2
@@ -2645,6 +2624,125 @@ packages:
cpu: [x64]
os: [win32]
+ '@oxc-parser/binding-android-arm-eabi@0.121.0':
+ resolution: {integrity: sha512-n07FQcySwOlzap424/PLMtOkbS7xOu8nsJduKL8P3COGHKgKoDYXwoAHCbChfgFpHnviehrLWIPX0lKGtbEk/A==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [android]
+
+ '@oxc-parser/binding-android-arm64@0.121.0':
+ resolution: {integrity: sha512-/Dd1xIXboYAicw+twT2utxPD7bL8qh7d3ej0qvaYIMj3/EgIrGR+tSnjCUkiCT6g6uTC0neSS4JY8LxhdSU/sA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [android]
+
+ '@oxc-parser/binding-darwin-arm64@0.121.0':
+ resolution: {integrity: sha512-A0jNEvv7QMtCO1yk205t3DWU9sWUjQ2KNF0hSVO5W9R9r/R1BIvzG01UQAfmtC0dQm7sCrs5puixurKSfr2bRQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@oxc-parser/binding-darwin-x64@0.121.0':
+ resolution: {integrity: sha512-SsHzipdxTKUs3I9EOAPmnIimEeJOemqRlRDOp9LIj+96wtxZejF51gNibmoGq8KoqbT1ssAI5po/E3J+vEtXGA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [darwin]
+
+ '@oxc-parser/binding-freebsd-x64@0.121.0':
+ resolution: {integrity: sha512-v1APOTkCp+RWOIDAHRoaeW/UoaHF15a60E8eUL6kUQXh+i4K7PBwq2Wi7jm8p0ymID5/m/oC1w3W31Z/+r7HQw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@oxc-parser/binding-linux-arm-gnueabihf@0.121.0':
+ resolution: {integrity: sha512-PmqPQuqHZyFVWA4ycr0eu4VnTMmq9laOHZd+8R359w6kzuNZPvmmunmNJ8ybkm769A0nCoVp3TJ6dUz7B3FYIQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@oxc-parser/binding-linux-arm-musleabihf@0.121.0':
+ resolution: {integrity: sha512-vF24htj+MOH+Q7y9A8NuC6pUZu8t/C2Fr/kDOi2OcNf28oogr2xadBPXAbml802E8wRAVfbta6YLDQTearz+jw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@oxc-parser/binding-linux-arm64-gnu@0.121.0':
+ resolution: {integrity: sha512-wjH8cIG2Lu/3d64iZpbYr73hREMgKAfu7fqpXjgM2S16y2zhTfDIp8EQjxO8vlDtKP5Rc7waZW72lh8nZtWrpA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+
+ '@oxc-parser/binding-linux-arm64-musl@0.121.0':
+ resolution: {integrity: sha512-qT663J/W8yQFw3dtscbEi9LKJevr20V7uWs2MPGTnvNZ3rm8anhhE16gXGpxDOHeg9raySaSHKhd4IGa3YZvuw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+
+ '@oxc-parser/binding-linux-ppc64-gnu@0.121.0':
+ resolution: {integrity: sha512-mYNe4NhVvDBbPkAP8JaVS8lC1dsoJZWH5WCjpw5E+sjhk1R08wt3NnXYUzum7tIiWPfgQxbCMcoxgeemFASbRw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@oxc-parser/binding-linux-riscv64-gnu@0.121.0':
+ resolution: {integrity: sha512-+QiFoGxhAbaI/amqX567784cDyyuZIpinBrJNxUzb+/L2aBRX67mN6Jv40pqduHf15yYByI+K5gUEygCuv0z9w==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@oxc-parser/binding-linux-riscv64-musl@0.121.0':
+ resolution: {integrity: sha512-9ykEgyTa5JD/Uhv2sttbKnCfl2PieUfOjyxJC/oDL2UO0qtXOtjPLl7H8Kaj5G7p3hIvFgu3YWvAxvE0sqY+hQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@oxc-parser/binding-linux-s390x-gnu@0.121.0':
+ resolution: {integrity: sha512-DB1EW5VHZdc1lIRjOI3bW/wV6R6y0xlfvdVrqj6kKi7Ayu2U3UqUBdq9KviVkcUGd5Oq+dROqvUEEFRXGAM7EQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [s390x]
+ os: [linux]
+
+ '@oxc-parser/binding-linux-x64-gnu@0.121.0':
+ resolution: {integrity: sha512-s4lfobX9p4kPTclvMiH3gcQUd88VlnkMTF6n2MTMDAyX5FPNRhhRSFZK05Ykhf8Zy5NibV4PbGR6DnK7FGNN6A==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+
+ '@oxc-parser/binding-linux-x64-musl@0.121.0':
+ resolution: {integrity: sha512-P9KlyTpuBuMi3NRGpJO8MicuGZfOoqZVRP1WjOecwx8yk4L/+mrCRNc5egSi0byhuReblBF2oVoDSMgV9Bj4Hw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+
+ '@oxc-parser/binding-openharmony-arm64@0.121.0':
+ resolution: {integrity: sha512-R+4jrWOfF2OAPPhj3Eb3U5CaKNAH9/btMveMULIrcNW/hjfysFQlF8wE0GaVBr81dWz8JLgQlsxwctoL78JwXw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@oxc-parser/binding-wasm32-wasi@0.121.0':
+ resolution: {integrity: sha512-5TFISkPTymKvsmIlKasPVTPuWxzCcrT8pM+p77+mtQbIZDd1UC8zww4CJcRI46kolmgrEX6QpKO8AvWMVZ+ifw==}
+ engines: {node: '>=14.0.0'}
+ cpu: [wasm32]
+
+ '@oxc-parser/binding-win32-arm64-msvc@0.121.0':
+ resolution: {integrity: sha512-V0pxh4mql4XTt3aiEtRNUeBAUFOw5jzZNxPABLaOKAWrVzSr9+XUaB095lY7jqMf5t8vkfh8NManGB28zanYKw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [win32]
+
+ '@oxc-parser/binding-win32-ia32-msvc@0.121.0':
+ resolution: {integrity: sha512-4Ob1qvYMPnlF2N9rdmKdkQFdrq16QVcQwBsO8yiPZXof0fHKFF+LmQV501XFbi7lHyrKm8rlJRfQ/M8bZZPVLw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [ia32]
+ os: [win32]
+
+ '@oxc-parser/binding-win32-x64-msvc@0.121.0':
+ resolution: {integrity: sha512-BOp1KCzdboB1tPqoCPXgntgFs0jjeSyOXHzgxVFR7B/qfr3F8r4YDacHkTOUNXtDgM8YwKnkf3rE5gwALYX7NA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [win32]
+
'@oxc-project/runtime@0.115.0':
resolution: {integrity: sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -2652,6 +2750,9 @@ packages:
'@oxc-project/types@0.115.0':
resolution: {integrity: sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==}
+ '@oxc-project/types@0.121.0':
+ resolution: {integrity: sha512-CGtOARQb9tyv7ECgdAlFxi0Fv7lmzvmlm2rpD/RdijOO9rfk/JvB1CjT8EnoD+tjna/IYgKKw3IV7objRb+aYw==}
+
'@oxc-resolver/binding-android-arm-eabi@11.16.4':
resolution: {integrity: sha512-6XUHilmj8D6Ggus+sTBp64x/DUQ7LgC/dvTDdUOt4iMQnDdSep6N1mnvVLIiG+qM5tRnNHravNzBJnUlYwRQoA==}
cpu: [arm]
@@ -7381,6 +7482,10 @@ packages:
resolution: {integrity: sha512-KWGTzPo83QmGrXC4ml83PM9HDwUPtZFfasiclUvTV4i3/0j7xRRqINVkrL77CbQnoWura3CMxkRofjQKVDuhBw==}
engines: {node: ^20.19.0 || >=22.12.0}
+ oxc-parser@0.121.0:
+ resolution: {integrity: sha512-ek9o58+SCv6AV7nchiAcUJy1DNE2CC5WRdBcO0mF+W4oRjNQfPO7b3pLjTHSFECpHkKGOZSQxx3hk8viIL5YCg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+
oxc-resolver@11.16.4:
resolution: {integrity: sha512-nvJr3orFz1wNaBA4neRw7CAn0SsjgVaEw1UHpgO/lzVW12w+nsFnvU/S6vVX3kYyFaZdxZheTExi/fa8R8PrZA==}
@@ -10743,10 +10848,74 @@ snapshots:
'@oxc-minify/binding-win32-x64-msvc@0.110.0':
optional: true
+ '@oxc-parser/binding-android-arm-eabi@0.121.0':
+ optional: true
+
+ '@oxc-parser/binding-android-arm64@0.121.0':
+ optional: true
+
+ '@oxc-parser/binding-darwin-arm64@0.121.0':
+ optional: true
+
+ '@oxc-parser/binding-darwin-x64@0.121.0':
+ optional: true
+
+ '@oxc-parser/binding-freebsd-x64@0.121.0':
+ optional: true
+
+ '@oxc-parser/binding-linux-arm-gnueabihf@0.121.0':
+ optional: true
+
+ '@oxc-parser/binding-linux-arm-musleabihf@0.121.0':
+ optional: true
+
+ '@oxc-parser/binding-linux-arm64-gnu@0.121.0':
+ optional: true
+
+ '@oxc-parser/binding-linux-arm64-musl@0.121.0':
+ optional: true
+
+ '@oxc-parser/binding-linux-ppc64-gnu@0.121.0':
+ optional: true
+
+ '@oxc-parser/binding-linux-riscv64-gnu@0.121.0':
+ optional: true
+
+ '@oxc-parser/binding-linux-riscv64-musl@0.121.0':
+ optional: true
+
+ '@oxc-parser/binding-linux-s390x-gnu@0.121.0':
+ optional: true
+
+ '@oxc-parser/binding-linux-x64-gnu@0.121.0':
+ optional: true
+
+ '@oxc-parser/binding-linux-x64-musl@0.121.0':
+ optional: true
+
+ '@oxc-parser/binding-openharmony-arm64@0.121.0':
+ optional: true
+
+ '@oxc-parser/binding-wasm32-wasi@0.121.0':
+ dependencies:
+ '@napi-rs/wasm-runtime': 1.1.1
+ optional: true
+
+ '@oxc-parser/binding-win32-arm64-msvc@0.121.0':
+ optional: true
+
+ '@oxc-parser/binding-win32-ia32-msvc@0.121.0':
+ optional: true
+
+ '@oxc-parser/binding-win32-x64-msvc@0.121.0':
+ optional: true
+
'@oxc-project/runtime@0.115.0': {}
'@oxc-project/types@0.115.0': {}
+ '@oxc-project/types@0.121.0': {}
+
'@oxc-resolver/binding-android-arm-eabi@11.16.4':
optional: true
@@ -16200,6 +16369,31 @@ snapshots:
'@oxc-minify/binding-win32-ia32-msvc': 0.110.0
'@oxc-minify/binding-win32-x64-msvc': 0.110.0
+ oxc-parser@0.121.0:
+ dependencies:
+ '@oxc-project/types': 0.121.0
+ optionalDependencies:
+ '@oxc-parser/binding-android-arm-eabi': 0.121.0
+ '@oxc-parser/binding-android-arm64': 0.121.0
+ '@oxc-parser/binding-darwin-arm64': 0.121.0
+ '@oxc-parser/binding-darwin-x64': 0.121.0
+ '@oxc-parser/binding-freebsd-x64': 0.121.0
+ '@oxc-parser/binding-linux-arm-gnueabihf': 0.121.0
+ '@oxc-parser/binding-linux-arm-musleabihf': 0.121.0
+ '@oxc-parser/binding-linux-arm64-gnu': 0.121.0
+ '@oxc-parser/binding-linux-arm64-musl': 0.121.0
+ '@oxc-parser/binding-linux-ppc64-gnu': 0.121.0
+ '@oxc-parser/binding-linux-riscv64-gnu': 0.121.0
+ '@oxc-parser/binding-linux-riscv64-musl': 0.121.0
+ '@oxc-parser/binding-linux-s390x-gnu': 0.121.0
+ '@oxc-parser/binding-linux-x64-gnu': 0.121.0
+ '@oxc-parser/binding-linux-x64-musl': 0.121.0
+ '@oxc-parser/binding-openharmony-arm64': 0.121.0
+ '@oxc-parser/binding-wasm32-wasi': 0.121.0
+ '@oxc-parser/binding-win32-arm64-msvc': 0.121.0
+ '@oxc-parser/binding-win32-ia32-msvc': 0.121.0
+ '@oxc-parser/binding-win32-x64-msvc': 0.121.0
+
oxc-resolver@11.16.4:
optionalDependencies:
'@oxc-resolver/binding-android-arm-eabi': 11.16.4