diff --git a/.changeset/major-lizards-notice.md b/.changeset/major-lizards-notice.md
new file mode 100644
index 00000000..509beff4
--- /dev/null
+++ b/.changeset/major-lizards-notice.md
@@ -0,0 +1,5 @@
+---
+'@tanstack/devtools-vite': patch
+---
+
+add ignore to inject source for granular manipulation
diff --git a/docs/vite-plugin.md b/docs/vite-plugin.md
index b13b017e..dcd316f3 100644
--- a/docs/vite-plugin.md
+++ b/docs/vite-plugin.md
@@ -165,7 +165,13 @@ export default {
plugins: [
devtools({
injectSource: {
- enabled: true
+ enabled: true,
+ ignore: {
+ // files to ignore source injection for
+ files: ['node_modules', /.*\.test\.(js|ts|jsx|tsx)$/],
+ // components to ignore source injection for
+ components: ['YourComponent', /.*Lazy$/],
+ },
}
}),
// ... rest of your plugins here
diff --git a/packages/devtools-vite/package.json b/packages/devtools-vite/package.json
index 6c491da0..8847bf4e 100644
--- a/packages/devtools-vite/package.json
+++ b/packages/devtools-vite/package.json
@@ -59,12 +59,14 @@
"@tanstack/devtools-client": "workspace:*",
"@tanstack/devtools-event-bus": "workspace:*",
"chalk": "^5.6.2",
- "launch-editor": "^2.11.1"
+ "launch-editor": "^2.11.1",
+ "picomatch": "^4.0.3"
},
"devDependencies": {
"@types/babel__core": "^7.20.5",
"@types/babel__generator": "^7.27.0",
"@types/babel__traverse": "^7.28.0",
+ "@types/picomatch": "^4.0.2",
"happy-dom": "^18.0.1"
}
}
diff --git a/packages/devtools-vite/src/inject-source.test.ts b/packages/devtools-vite/src/inject-source.test.ts
index d8c8d55c..f2a344aa 100644
--- a/packages/devtools-vite/src/inject-source.test.ts
+++ b/packages/devtools-vite/src/inject-source.test.ts
@@ -854,4 +854,36 @@ function test({...rest}) {
)
})
})
+
+ describe('ignore patterns', () => {
+ it('should skip injection for ignored component names (string)', () => {
+ const output = addSourceToJsx(
+ `
+ function test() {
+ return
+ }
+ `,
+ 'test.jsx',
+ {
+ components: ['Button'],
+ },
+ )
+ expect(output).toBe(undefined)
+ })
+
+ it('should skip injection for ignored file paths (glob)', () => {
+ const output = addSourceToJsx(
+ `
+ function test() {
+ return
+ }
+ `,
+ 'src/components/ignored-file.jsx',
+ {
+ files: ['**/ignored-file.jsx'],
+ },
+ )
+ expect(output).toBe(undefined)
+ })
+ })
})
diff --git a/packages/devtools-vite/src/inject-source.ts b/packages/devtools-vite/src/inject-source.ts
index 0b4cbe7d..28502e3b 100644
--- a/packages/devtools-vite/src/inject-source.ts
+++ b/packages/devtools-vite/src/inject-source.ts
@@ -1,5 +1,6 @@
import { normalizePath } from 'vite'
import { gen, parse, t, trav } from './babel'
+import { matcher } from './matcher'
import type { types as Babel, NodePath } from '@babel/core'
import type { ParseResult } from '@babel/parser'
@@ -111,14 +112,19 @@ 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)
-
- if (nameOfElement === 'Fragment' || nameOfElement === 'React.Fragment') {
+ const isIgnored = matcher(ignorePatterns, nameOfElement)
+ if (
+ nameOfElement === 'Fragment' ||
+ nameOfElement === 'React.Fragment' ||
+ isIgnored
+ ) {
return
}
const hasDataSource = element.node.attributes.some(
@@ -151,7 +157,11 @@ const transformJSX = (
return true
}
-const transform = (ast: ParseResult, file: string) => {
+const transform = (
+ ast: ParseResult,
+ file: string,
+ ignorePatterns: Array,
+) => {
let didTransform = false
trav(ast, {
@@ -161,7 +171,12 @@ const transform = (ast: ParseResult, file: string) => {
)
functionDeclaration.traverse({
JSXOpeningElement(element) {
- const transformed = transformJSX(element, propsName, file)
+ const transformed = transformJSX(
+ element,
+ propsName,
+ file,
+ ignorePatterns,
+ )
if (transformed) {
didTransform = true
}
@@ -172,7 +187,12 @@ const transform = (ast: ParseResult, file: string) => {
const propsName = getPropsNameFromFunctionDeclaration(path.node)
path.traverse({
JSXOpeningElement(element) {
- const transformed = transformJSX(element, propsName, file)
+ const transformed = transformJSX(
+ element,
+ propsName,
+ file,
+ ignorePatterns,
+ )
if (transformed) {
didTransform = true
}
@@ -183,7 +203,12 @@ const transform = (ast: ParseResult, file: string) => {
const propsName = getPropsNameFromFunctionDeclaration(path.node)
path.traverse({
JSXOpeningElement(element) {
- const transformed = transformJSX(element, propsName, file)
+ const transformed = transformJSX(
+ element,
+ propsName,
+ file,
+ ignorePatterns,
+ )
if (transformed) {
didTransform = true
}
@@ -204,7 +229,12 @@ const transform = (ast: ParseResult, file: string) => {
path.traverse({
JSXOpeningElement(element) {
- const transformed = transformJSX(element, propsName, file)
+ const transformed = transformJSX(
+ element,
+ propsName,
+ file,
+ ignorePatterns,
+ )
if (transformed) {
didTransform = true
}
@@ -216,17 +246,28 @@ const transform = (ast: ParseResult, file: string) => {
return didTransform
}
-export function addSourceToJsx(code: string, id: string) {
+export function addSourceToJsx(
+ code: string,
+ id: string,
+ ignore: {
+ files?: Array
+ components?: Array
+ } = {},
+) {
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) {
+ return
+ }
try {
const ast = parse(code, {
sourceType: 'module',
plugins: ['jsx', 'typescript'],
})
- const didTransform = transform(ast, location)
+ const didTransform = transform(ast, location, ignore.components || [])
if (!didTransform) {
return
}
diff --git a/packages/devtools-vite/src/matcher.test.ts b/packages/devtools-vite/src/matcher.test.ts
new file mode 100644
index 00000000..eced3535
--- /dev/null
+++ b/packages/devtools-vite/src/matcher.test.ts
@@ -0,0 +1,33 @@
+import { describe, expect, it } from 'vitest'
+import { matcher } from './matcher'
+
+describe('matcher', () => {
+ it('returns false when patterns is empty', () => {
+ expect(matcher([], 'foo')).toBe(false)
+ })
+
+ it('matches string patterns against component names', () => {
+ const patterns = ['Button', 'MyComponent']
+ expect(matcher(patterns, 'Button')).toBe(true)
+ expect(matcher(patterns, 'Other')).toBe(false)
+ })
+
+ it('matches regex patterns against component names', () => {
+ const patterns = [/^Button$/, /Comp/]
+ expect(matcher(patterns, 'Button')).toBe(true)
+ expect(matcher(patterns, 'MyComp')).toBe(true)
+ expect(matcher(patterns, 'NoMatch')).toBe(false)
+ })
+
+ it('matches file paths with glob patterns', () => {
+ const patterns = ['**/ignored-file.jsx']
+ expect(matcher(patterns, 'src/components/ignored-file.jsx')).toBe(true)
+ expect(matcher(patterns, 'src/components/other.jsx')).toBe(false)
+ })
+
+ it('matches file paths with regex', () => {
+ const patterns = [/ignored-file\.jsx$/]
+ expect(matcher(patterns, 'src/components/ignored-file.jsx')).toBe(true)
+ expect(matcher(patterns, 'src/components/other.jsx')).toBe(false)
+ })
+})
diff --git a/packages/devtools-vite/src/matcher.ts b/packages/devtools-vite/src/matcher.ts
new file mode 100644
index 00000000..0098d763
--- /dev/null
+++ b/packages/devtools-vite/src/matcher.ts
@@ -0,0 +1,19 @@
+import picomatch from 'picomatch'
+
+export const matcher = (
+ patterns: Array,
+ str: string,
+): boolean => {
+ if (patterns.length === 0) {
+ return false
+ }
+ const matchers = patterns.map((pattern) => {
+ if (typeof pattern === 'string') {
+ return picomatch(pattern)
+ } else {
+ return (s: string) => pattern.test(s)
+ }
+ })
+
+ return matchers.some((isMatch) => isMatch(str))
+}
diff --git a/packages/devtools-vite/src/plugin.ts b/packages/devtools-vite/src/plugin.ts
index 665ee83c..6c82c3a5 100644
--- a/packages/devtools-vite/src/plugin.ts
+++ b/packages/devtools-vite/src/plugin.ts
@@ -62,6 +62,13 @@ export type TanStackDevtoolsViteConfig = {
* @default true
*/
enabled: boolean
+ /**
+ * List of files or patterns to ignore for source injection.
+ */
+ ignore?: {
+ files?: Array
+ components?: Array
+ }
}
}
@@ -95,7 +102,7 @@ export const devtools = (args?: TanStackDevtoolsViteConfig): Array => {
)
return
- return addSourceToJsx(code, id)
+ return addSourceToJsx(code, id, args?.injectSource?.ignore)
},
},
{
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 909c2a2d..d39988f5 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -388,6 +388,8 @@ importers:
specifier: ^4.2.4
version: 4.2.4
+ examples/react/start/generated/prisma: {}
+
examples/react/time-travel:
dependencies:
'@tanstack/devtools-event-client':
@@ -599,6 +601,9 @@ importers:
launch-editor:
specifier: ^2.11.1
version: 2.11.1
+ picomatch:
+ specifier: ^4.0.3
+ version: 4.0.3
vite:
specifier: ^6.0.0 || ^7.0.0
version: 7.1.7(@types/node@22.15.2)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)
@@ -612,6 +617,9 @@ importers:
'@types/babel__traverse':
specifier: ^7.28.0
version: 7.28.0
+ '@types/picomatch':
+ specifier: ^4.0.2
+ version: 4.0.2
happy-dom:
specifier: ^18.0.1
version: 18.0.1
@@ -3128,6 +3136,9 @@ packages:
'@types/parse-json@4.0.2':
resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==}
+ '@types/picomatch@4.0.2':
+ resolution: {integrity: sha512-qHHxQ+P9PysNEGbALT8f8YOSHW0KJu6l2xU8DYY0fu/EmGxXdVnuTLvFUvBgPJMSqXq29SYHveejeAha+4AYgA==}
+
'@types/prop-types@15.7.15':
resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==}
@@ -10498,6 +10509,8 @@ snapshots:
'@types/parse-json@4.0.2': {}
+ '@types/picomatch@4.0.2': {}
+
'@types/prop-types@15.7.15': {}
'@types/react-dom@19.1.9(@types/react@19.1.13)':