diff --git a/.changeset/devtools-vite-transform-filters.md b/.changeset/devtools-vite-transform-filters.md new file mode 100644 index 000000000..2b3cac9cf --- /dev/null +++ b/.changeset/devtools-vite-transform-filters.md @@ -0,0 +1,5 @@ +--- +'@tanstack/devtools-vite': patch +--- + +Give every transform hook a `filter`, so Vite and Rolldown skip the modules its handler would ignore instead of calling it on every module. Source injection no longer parses `.ts`, `.mts` and `.cts` files, which cannot contain JSX, and the devtools setup file is no longer looked for in `node_modules`. diff --git a/packages/devtools-vite/skills/devtools-vite-plugin/SKILL.md b/packages/devtools-vite/skills/devtools-vite-plugin/SKILL.md index 960f9d91b..4d4e45e80 100644 --- a/packages/devtools-vite/skills/devtools-vite-plugin/SKILL.md +++ b/packages/devtools-vite/skills/devtools-vite-plugin/SKILL.md @@ -226,7 +226,7 @@ When the dev server is running, listens for events via `devtoolsEventClient`: - `bump-package-version` -- updates a package to a minimum version - `mounted` -- sends package.json and outdated deps to the UI -Auto-detection of the devtools setup file: the `inject-plugin` sub-plugin scans transforms for files importing from `@tanstack/react-devtools`, `@tanstack/solid-devtools`, `@tanstack/vue-devtools`, etc., and stores the file ID. +Auto-detection of the devtools setup file: the `inject-plugin` sub-plugin scans transformed modules outside `node_modules` for files importing from `@tanstack/react-devtools`, `@tanstack/solid-devtools`, `@tanstack/vue-devtools`, etc., and stores the file ID. **Source files:** `packages/devtools-vite/src/inject-plugin.ts`, `packages/devtools-vite/src/package-manager.ts` 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 90fd0247e..eda5fdaba 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 @@ -58,6 +58,7 @@ Controls source injection -- the AST transform that adds `data-tsd-source` attri - `?raw` imports - `/dist/` paths - `/build/` paths +- `.ts`, `.mts` and `.cts` files, which cannot contain JSX - `` and `` elements - Elements with `{...propsParam}` spread (where `propsParam` is the function's parameter name) diff --git a/packages/devtools-vite/src/plugin.ts b/packages/devtools-vite/src/plugin.ts index 8c7acdd65..01940e67f 100644 --- a/packages/devtools-vite/src/plugin.ts +++ b/packages/devtools-vite/src/plugin.ts @@ -52,6 +52,9 @@ export const devtools = (args?: TanStackDevtoolsViteConfig): Array => { let devtoolsHost: string | null = null let devtoolsProtocol: 'http' | 'https' | null = null + // Each transform hook has a `filter`, so Vite and Rolldown skip the modules + // its handler would ignore. The handlers keep their own checks, so Vite + // before 6.3, which ignores `filter`, behaves as before. return [ { enforce: 'pre', @@ -62,8 +65,17 @@ export const devtools = (args?: TanStackDevtoolsViteConfig): Array => { transform: { filter: { id: { - exclude: [/node_modules/, /\?raw/, /\/dist\//, /\/build\//], + exclude: [ + /node_modules/, + /\?raw/, + /\/dist\//, + /\/build\//, + // `.ts`, `.mts` and `.cts` files cannot contain JSX. + /\.[cm]?ts(?:$|\?)/, + ], }, + // Every JSX element starts with `<`. + code: '<', }, handler(code, id) { return addSourceToJsx(code, id, args?.injectSource?.ignore) @@ -259,21 +271,27 @@ export const devtools = (args?: TanStackDevtoolsViteConfig): Array => { ) }, enforce: 'pre', - transform(code, id) { - if ( - id.includes('node_modules') || - id.includes('?raw') || - !TANSTACK_DEVTOOLS_PACKAGES.some((pkg) => code.includes(pkg)) - ) - return - const transform = removeDevtools(code, id) - if (!transform) return - if (logging) { - console.log( - `\n${chalk.greenBright(`[@tanstack/devtools-vite]`)} Removed devtools code from: ${id.replace(normalizePath(process.cwd()), '')}\n`, + transform: { + filter: { + id: { exclude: [/node_modules/, /\?raw/] }, + code: [...TANSTACK_DEVTOOLS_PACKAGES], + }, + handler(code, id) { + if ( + id.includes('node_modules') || + id.includes('?raw') || + !TANSTACK_DEVTOOLS_PACKAGES.some((pkg) => code.includes(pkg)) ) - } - return transform + return + const transform = removeDevtools(code, id) + if (!transform) return + if (logging) { + console.log( + `\n${chalk.greenBright(`[@tanstack/devtools-vite]`)} Removed devtools code from: ${id.replace(normalizePath(process.cwd()), '')}\n`, + ) + } + return transform + }, }, }, { @@ -523,6 +541,14 @@ export const devtools = (args?: TanStackDevtoolsViteConfig): Array => { exclude: [/node_modules/, /\/dist\//, /\?/], }, code: { + // The root entry markers the handler looks for + include: [ + /]/i, + 'StartClient', + 'hydrateRoot', + 'createRoot', + 'solid-js/web', + ], exclude: /__tsdConsolePipe/, // avoid transforming files that already contain the console pipe code }, }, @@ -556,17 +582,24 @@ export const devtools = (args?: TanStackDevtoolsViteConfig): Array => { apply(config, { command }) { return config.mode === 'development' && command === 'serve' }, - transform(code, id) { - // First pass: find where TanStackDevtools is imported - if (!devtoolsFileId && detectDevtoolsFile(code)) { - // Extract actual file path (remove query params) - const [filePath] = id.split('?') - if (filePath) { - devtoolsFileId = filePath + transform: { + filter: { + // The user's devtools setup is never an installed package. + id: { exclude: [/node_modules/] }, + code: [...TANSTACK_DEVTOOLS_PACKAGES], + }, + handler(code, id) { + // First pass: find where TanStackDevtools is imported + if (!devtoolsFileId && detectDevtoolsFile(code)) { + // Extract actual file path (remove query params) + const [filePath] = id.split('?') + if (filePath) { + devtoolsFileId = filePath + } } - } - return undefined + return undefined + }, }, }, { @@ -574,38 +607,44 @@ export const devtools = (args?: TanStackDevtoolsViteConfig): Array => { apply(config, { command }) { return config.mode === 'development' && command === 'serve' }, - transform(code, id) { - // Only transform @tanstack packages that contain the connection placeholders - const hasPlaceholder = - code.includes('__TANSTACK_DEVTOOLS_PORT__') || - code.includes('__TANSTACK_DEVTOOLS_HOST__') || - code.includes('__TANSTACK_DEVTOOLS_PROTOCOL__') - if (!hasPlaceholder) return - if ( - !id.includes('@tanstack/devtools') && - !id.includes('@tanstack/event-bus') - ) - return + transform: { + filter: { + id: /@tanstack\/(?:devtools|event-bus)/, + code: /__TANSTACK_DEVTOOLS_(?:PORT|HOST|PROTOCOL)__/, + }, + handler(code, id) { + // Only transform @tanstack packages that contain the connection placeholders + const hasPlaceholder = + code.includes('__TANSTACK_DEVTOOLS_PORT__') || + code.includes('__TANSTACK_DEVTOOLS_HOST__') || + code.includes('__TANSTACK_DEVTOOLS_PROTOCOL__') + if (!hasPlaceholder) return + if ( + !id.includes('@tanstack/devtools') && + !id.includes('@tanstack/event-bus') + ) + return - // Replace placeholders with actual values (or fallback defaults) - const portValue = devtoolsPort ?? 4206 - const hostValue = devtoolsHost ?? 'localhost' - const protocolValue = devtoolsProtocol ?? 'http' + // Replace placeholders with actual values (or fallback defaults) + const portValue = devtoolsPort ?? 4206 + const hostValue = devtoolsHost ?? 'localhost' + const protocolValue = devtoolsProtocol ?? 'http' - let result = code - result = result.replace( - /__TANSTACK_DEVTOOLS_PORT__/g, - String(portValue), - ) - result = result.replace( - /__TANSTACK_DEVTOOLS_HOST__/g, - JSON.stringify(hostValue), - ) - result = result.replace( - /__TANSTACK_DEVTOOLS_PROTOCOL__/g, - JSON.stringify(protocolValue), - ) - return result + let result = code + result = result.replace( + /__TANSTACK_DEVTOOLS_PORT__/g, + String(portValue), + ) + result = result.replace( + /__TANSTACK_DEVTOOLS_HOST__/g, + JSON.stringify(hostValue), + ) + result = result.replace( + /__TANSTACK_DEVTOOLS_PROTOCOL__/g, + JSON.stringify(protocolValue), + ) + return result + }, }, }, { @@ -613,13 +652,23 @@ export const devtools = (args?: TanStackDevtoolsViteConfig): Array => { apply(config, { command }) { return config.mode === 'development' && command === 'serve' }, - transform(code, id) { - if (id.includes('?')) return - return injectRuntimeBridge( - code, - id, - (this as any).environment?.name as string | undefined, - ) + transform: { + // The modules `injectRuntimeBridge` accepts, outside a query. + filter: { + id: { + include: /devtools-event-client|event-bus-client/, + exclude: /\?/, + }, + code: 'EventClient', + }, + handler(code, id) { + if (id.includes('?')) return + return injectRuntimeBridge( + code, + id, + (this as any).environment?.name as string | undefined, + ) + }, }, }, ] diff --git a/packages/devtools-vite/tests/index.test.ts b/packages/devtools-vite/tests/index.test.ts index 47571da28..e23947763 100644 --- a/packages/devtools-vite/tests/index.test.ts +++ b/packages/devtools-vite/tests/index.test.ts @@ -24,6 +24,13 @@ function findPlugin(plugins: Array, name: string): Plugin | undefined { return plugins.find((p) => p.name === name) } +// Helper to call a plugin's transform handler directly, without its filter +function getTransform(plugin: Plugin) { + const hook = plugin.transform + const handler = typeof hook === 'function' ? hook : hook?.handler + return handler as unknown as (code: string, id: string) => string | undefined +} + // Helper to create a mock Vite server function createMockServer( options: { @@ -87,10 +94,7 @@ describe('devtools plugin', () => { }) it('should replace __TANSTACK_DEVTOOLS_PORT__ in matching modules', () => { - const transform = connectionPlugin.transform as ( - code: string, - id: string, - ) => string | undefined + const transform = getTransform(connectionPlugin) const code = 'const port = __TANSTACK_DEVTOOLS_PORT__' const result = transform( code, @@ -103,10 +107,7 @@ describe('devtools plugin', () => { }) it('should replace __TANSTACK_DEVTOOLS_HOST__ in matching modules', () => { - const transform = connectionPlugin.transform as ( - code: string, - id: string, - ) => string | undefined + const transform = getTransform(connectionPlugin) const code = 'const host = __TANSTACK_DEVTOOLS_HOST__' const result = transform( code, @@ -119,10 +120,7 @@ describe('devtools plugin', () => { }) it('should replace __TANSTACK_DEVTOOLS_PROTOCOL__ in matching modules', () => { - const transform = connectionPlugin.transform as ( - code: string, - id: string, - ) => string | undefined + const transform = getTransform(connectionPlugin) const code = 'const protocol = __TANSTACK_DEVTOOLS_PROTOCOL__' const result = transform( code, @@ -135,10 +133,7 @@ describe('devtools plugin', () => { }) it('should replace all three placeholders in the same code', () => { - const transform = connectionPlugin.transform as ( - code: string, - id: string, - ) => string | undefined + const transform = getTransform(connectionPlugin) const code = [ 'const port = __TANSTACK_DEVTOOLS_PORT__;', 'const host = __TANSTACK_DEVTOOLS_HOST__;', @@ -155,10 +150,7 @@ describe('devtools plugin', () => { }) it('should return undefined for code without any placeholders', () => { - const transform = connectionPlugin.transform as ( - code: string, - id: string, - ) => string | undefined + const transform = getTransform(connectionPlugin) const result = transform( 'const x = 42', 'node_modules/@tanstack/event-bus/dist/client.js', @@ -167,10 +159,7 @@ describe('devtools plugin', () => { }) it('should return undefined for non-tanstack module IDs', () => { - const transform = connectionPlugin.transform as ( - code: string, - id: string, - ) => string | undefined + const transform = getTransform(connectionPlugin) const result = transform( 'const port = __TANSTACK_DEVTOOLS_PORT__', 'node_modules/some-other-package/index.js', @@ -216,10 +205,7 @@ describe('devtools plugin', () => { await configureServer(server) // Now test the transform to verify protocol was set to 'https' - const transform = connectionPlugin!.transform as ( - code: string, - id: string, - ) => string | undefined + const transform = getTransform(connectionPlugin!) const result = transform( 'const protocol = __TANSTACK_DEVTOOLS_PROTOCOL__', 'node_modules/@tanstack/event-bus/dist/client.js', @@ -250,10 +236,7 @@ describe('devtools plugin', () => { ) => Promise await configureServer(server) - const transform = connectionPlugin!.transform as ( - code: string, - id: string, - ) => string | undefined + const transform = getTransform(connectionPlugin!) const result = transform( 'const protocol = __TANSTACK_DEVTOOLS_PROTOCOL__', 'node_modules/@tanstack/event-bus/dist/client.js', @@ -284,10 +267,7 @@ describe('devtools plugin', () => { ) => Promise await configureServer(server) - const transform = connectionPlugin!.transform as ( - code: string, - id: string, - ) => string | undefined + const transform = getTransform(connectionPlugin!) const result = transform( 'const host = __TANSTACK_DEVTOOLS_HOST__', 'node_modules/@tanstack/event-bus/dist/client.js', @@ -318,10 +298,7 @@ describe('devtools plugin', () => { ) => Promise await configureServer(server) - const transform = connectionPlugin!.transform as ( - code: string, - id: string, - ) => string | undefined + const transform = getTransform(connectionPlugin!) const result = transform( 'const host = __TANSTACK_DEVTOOLS_HOST__', 'node_modules/@tanstack/event-bus/dist/client.js', diff --git a/packages/devtools-vite/tests/transform-filters.test.ts b/packages/devtools-vite/tests/transform-filters.test.ts new file mode 100644 index 000000000..ac29aaa52 --- /dev/null +++ b/packages/devtools-vite/tests/transform-filters.test.ts @@ -0,0 +1,373 @@ +// @vitest-environment node +import { + mkdirSync, + mkdtempSync, + realpathSync, + rmSync, + writeFileSync, +} from 'node:fs' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' +import { detectDevtoolsFile } from '@tanstack/devtools-bundler-core' +import { createServer } from 'vite' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' +import { devtools } from '../src/plugin' +import type { Plugin, ViteDevServer } from 'vite' + +type Pattern = string | RegExp +type StringFilter = + | Pattern + | Array + | { include?: Pattern | Array; exclude?: Pattern | Array } +type Filter = { id?: StringFilter; code?: StringFilter } +type Handler = ( + this: unknown, + code: string, + id: string, +) => string | { code: string } | undefined | null + +// Vite's rules: `exclude` wins, then an `include`, if given, must match. A +// RegExp is tested on the value. A string in `code` means "contains"; in `id` +// it would be a glob, which these filters do not use. +function matches( + filter: StringFilter | undefined, + value: string, + kind: 'id' | 'code', +) { + if (filter === undefined) return true + const { include, exclude } = + typeof filter === 'string' || + filter instanceof RegExp || + Array.isArray(filter) + ? { include: filter, exclude: undefined } + : filter + const test = (pattern: Pattern) => { + if (typeof pattern !== 'string') return pattern.test(value) + if (kind === 'id') throw new Error(`glob id filter: ${pattern}`) + return value.includes(pattern) + } + if (exclude !== undefined && [exclude].flat().some(test)) return false + const includes = include === undefined ? [] : [include].flat() + return includes.length === 0 || includes.some(test) +} + +function transformHook(plugins: Array, name: string) { + const hook = plugins.find((p) => p.name === name)?.transform + if (!hook || typeof hook === 'function' || !hook.filter) { + throw new Error(`${name} has no transform filter`) + } + const filter = hook.filter as Filter + return { + passes: (id: string, code: string) => + matches(filter.id, id, 'id') && matches(filter.code, code, 'code'), + handler: hook.handler as unknown as Handler, + } +} + +const plugins = devtools({ logging: false }) +const ssr = { environment: { name: 'ssr' } } + +describe('transform hook filters', () => { + it('every transform hook has a filter', () => { + const unfiltered = plugins + .filter((p) => p.transform) + .filter((p) => typeof p.transform === 'function' || !p.transform!.filter) + .map((p) => p.name) + expect(unfiltered).toEqual([]) + }) + + describe('inject-source', () => { + const { passes, handler } = transformHook( + plugins, + '@tanstack/devtools:inject-source', + ) + + it.each([ + ['/app/src/App.tsx', 'export const App = () =>
hi
'], + ['/app/src/App.jsx', 'export const App = () =>
hi
'], + ['/app/src/legacy.js', 'export const App = () =>
hi
'], + ['/app/src/spaced.jsx', 'export const App = () => < div>hi'], + [ + '/app/src/routes/index.tsx?tsr-split=component', + 'const Route = () =>
', + ], + [ + '/app/src/Card.vue?vue&type=script&setup=true&lang.tsx', + 'export default () =>
', + ], + [ + '/app/src/post.mdx', + 'export default function MDXContent() { return

Hi

}', + ], + ])('runs on %s', (id, code) => { + expect(handler(code, id)).toBeDefined() + expect(passes(id, code)).toBe(true) + }) + + it.each([ + ['/app/src/ids.ts', 'export const ids = () => new Map()'], + ['/app/src/api.mts', 'export const get = (url: string) => url'], + ['/app/src/api.cts?v=1', 'export const x = (y: T) => y'], + ['/app/src/env.d.ts', 'declare const a: Array'], + [ + '/app/src/Card.vue?vue&type=script&setup=true&lang.ts', + 'const items = ref>([])', + ], + ['/app/src/math.js', 'export const add = (a, b) => a + b'], + ['/app/src/styles.css', 'a { color: red }'], + ['/app/src/data.json', '{ "a": 1 }'], + ])('skips %s', (id, code) => { + expect(handler(code, id)).toBeUndefined() + expect(passes(id, code)).toBe(false) + }) + }) + + describe('remove-devtools-on-build', () => { + const { passes, handler } = transformHook( + plugins, + '@tanstack/devtools:remove-devtools-on-build', + ) + const app = [ + `import { TanStackDevtools } from '@tanstack/react-devtools'`, + `export const App = () => `, + ].join('\n') + + it('runs on a file that imports devtools', () => { + expect(handler(app, '/app/src/App.tsx')).toBeDefined() + expect(passes('/app/src/App.tsx', app)).toBe(true) + }) + + it.each([ + ['/app/src/App.tsx', 'export const App = () =>
'], + ['/app/src/App.tsx?raw', app], + ['/app/node_modules/lib/dist/index.js', app], + ])('skips %s', (id, code) => { + expect(handler(code, id)).toBeUndefined() + expect(passes(id, code)).toBe(false) + }) + }) + + describe('console-pipe-transform', () => { + const { passes, handler } = transformHook( + plugins, + '@tanstack/devtools:console-pipe-transform', + ) + + it.each([ + ['/app/src/main.tsx', `createRoot(el).render()`], + ['/app/src/entry-client.tsx', `hydrateRoot(document, )`], + ['/app/src/client.tsx', `hydrateRoot(document, )`], + ['/app/src/routes/__root.tsx', `const Doc = () => `], + ['/app/src/root.jsx', `const Doc = () => {children}`], + [ + '/app/src/index.tsx', + `import { render } from 'solid-js/web'\nrender(() => , el)`, + ], + ])('runs on %s', (id, code) => { + expect(handler(code, id)).toBeDefined() + expect(passes(id, code)).toBe(true) + }) + + it.each([ + ['/app/src/App.tsx', 'export const App = () =>
'], + ['/app/src/utils.ts', 'export const add = (a: number) => a + 1'], + ])('skips %s', (id, code) => { + expect(handler(code, id)).toBeUndefined() + expect(passes(id, code)).toBe(false) + }) + }) + + describe('inject-plugin', () => { + const { passes } = transformHook( + plugins, + '@tanstack/devtools:inject-plugin', + ) + + it.each([ + [ + '/app/src/routes/__root.tsx', + `import { TanStackDevtools } from '@tanstack/react-devtools'`, + ], + [ + '/app/src/main.tsx', + `import { jsxDEV } from "react/jsx-dev-runtime";\nimport { TanStackDevtools } from "@tanstack/react-devtools";`, + ], + [ + '/app/src/index.tsx', + `import { TanStackDevtools } from '@tanstack/solid-devtools'`, + ], + [ + '/app/src/main.ts', + `import { TanStackDevtoolsCore } from '@tanstack/devtools'`, + ], + ])('runs on %s', (id, code) => { + expect(detectDevtoolsFile(code)).toBe(true) + expect(passes(id, code)).toBe(true) + }) + + it.each([ + ['/app/src/App.tsx', `import { useState } from 'react'`], + ['/app/src/utils.ts', 'export const add = (a: number) => a + 1'], + ])('skips %s', (id, code) => { + expect(detectDevtoolsFile(code)).toBe(false) + expect(passes(id, code)).toBe(false) + }) + + it('skips installed packages that import devtools', () => { + const code = `import { TanStackDevtools } from '@tanstack/react-devtools'` + expect(passes('/app/node_modules/@acme/shell/dist/index.js', code)).toBe( + false, + ) + }) + }) + + describe('connection-injection', () => { + const { passes, handler } = transformHook( + plugins, + '@tanstack/devtools:connection-injection', + ) + + it.each([ + [ + '/app/node_modules/@tanstack/event-bus/dist/esm/client/client.js', + 'const port = __TANSTACK_DEVTOOLS_PORT__', + ], + [ + '/app/node_modules/.pnpm/@tanstack+devtools@0.10.0/node_modules/@tanstack/devtools/dist/esm/index.js', + 'const host = __TANSTACK_DEVTOOLS_HOST__', + ], + [ + '/app/node_modules/@tanstack/devtools-event-client/dist/esm/index.js', + 'const protocol = __TANSTACK_DEVTOOLS_PROTOCOL__', + ], + ])('runs on %s', (id, code) => { + expect(handler(code, id)).toBeDefined() + expect(passes(id, code)).toBe(true) + }) + + it.each([ + [ + '/app/node_modules/some-other-package/index.js', + 'const port = __TANSTACK_DEVTOOLS_PORT__', + ], + [ + '/app/node_modules/@tanstack/event-bus/dist/esm/index.js', + 'const x = 1', + ], + ['/app/src/App.tsx', 'export const App = () =>
'], + ])('skips %s', (id, code) => { + expect(handler(code, id)).toBeUndefined() + expect(passes(id, code)).toBe(false) + }) + }) + + describe('runtime-bridge', () => { + const { passes, handler } = transformHook( + plugins, + '@tanstack/devtools:runtime-bridge', + ) + + it.each([ + [ + '/app/node_modules/@tanstack/devtools-event-client/dist/esm/plugin.js', + 'export class EventClient {}', + ], + ['/app/packages/event-bus-client/src/index.js', 'class EventClient {}'], + ])('runs on %s', (id, code) => { + expect(handler.call(ssr, code, id)).toBeDefined() + expect(passes(id, code)).toBe(true) + }) + + it.each([ + [ + '/app/node_modules/@tanstack/devtools-event-client/dist/esm/plugin.js?v=1', + 'export class EventClient {}', + ], + [ + '/app/node_modules/@tanstack/devtools-event-client/dist/esm/index.js', + `export * from './plugin.js'`, + ], + ['/app/src/client.ts', 'export class EventClient {}'], + ])('skips %s', (id, code) => { + expect(handler.call(ssr, code, id)).toBeUndefined() + expect(passes(id, code)).toBe(false) + }) + }) +}) + +describe('transform hook filters in a Vite dev server', () => { + let root: string + let server: ViteDevServer + const calls: Record> = {} + + const write = (file: string, code: string) => { + mkdirSync(dirname(join(root, file)), { recursive: true }) + writeFileSync(join(root, file), code) + } + + beforeAll(async () => { + // The real path, as Vite resolves ids through symlinks such as macOS /var. + root = realpathSync(mkdtempSync(join(tmpdir(), 'tsd-filters-'))) + write('App.tsx', 'export const App = () =>
hi
\n') + write('ids.ts', 'export const ids = () => new Map()\n') + write('style.css', 'a { color: red }\n') + write( + 'node_modules/@tanstack/event-bus/dist/client.js', + 'export const port = __TANSTACK_DEVTOOLS_PORT__\n', + ) + write( + 'node_modules/other/index.js', + 'export const port = __TANSTACK_DEVTOOLS_PORT__\n', + ) + + const devtoolsPlugins = devtools({ + eventBusConfig: { enabled: false }, + consolePiping: { enabled: false }, + }) + // Record which modules reach each handler. + for (const plugin of devtoolsPlugins) { + const hook = plugin.transform + if (!hook || typeof hook === 'function') continue + const handler = hook.handler as Handler + ;(hook as { handler: Handler }).handler = function (code, id) { + ;(calls[plugin.name] ??= []).push(id.slice(root.length).split('?')[0]!) + return handler.call(this, code, id) + } + } + + server = await createServer({ + root, + mode: 'development', + configFile: false, + logLevel: 'silent', + server: { middlewareMode: true, ws: false }, + // The classic runtime needs no react import. + oxc: { jsx: { runtime: 'classic' } }, + plugins: devtoolsPlugins, + }) + }) + + afterAll(async () => { + await server.close() + rmSync(root, { recursive: true, force: true }) + }) + + it('calls the handlers only for the modules they transform', async () => { + const app = await server.transformRequest('/App.tsx') + await server.transformRequest('/ids.ts') + await server.transformRequest('/style.css') + const bus = await server.transformRequest( + '/node_modules/@tanstack/event-bus/dist/client.js', + ) + await server.transformRequest('/node_modules/other/index.js') + + expect(app?.code).toContain('data-tsd-source') + expect(bus?.code).toContain('4206') + expect(calls['@tanstack/devtools:inject-source']).toEqual(['/App.tsx']) + expect(calls['@tanstack/devtools:connection-injection']).toEqual([ + '/node_modules/@tanstack/event-bus/dist/client.js', + ]) + expect(calls['@tanstack/devtools:inject-plugin']).toBeUndefined() + expect(calls['@tanstack/devtools:runtime-bridge']).toBeUndefined() + }) +})