From d5e0e2e3d3a8f302bf0a204c9ddc0fcf56af5197 Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Mon, 18 May 2026 08:09:59 +0530 Subject: [PATCH 1/2] feat(plugin-vue-jsx): inject __file on components in dev/HMR mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `@vitejs/plugin-vue` sets `__file` on every SFC component in dev mode, enabling Vue DevTools' component inspector and open-in-editor to resolve source files. `plugin-vue-jsx` was only injecting `__hmrId` — so JSX/TSX components were invisible to devtools file-open features. Inject `__file` alongside `__hmrId` whenever HMR metadata is written, mirroring the SFC plugin's behavior. Scoped to `needHmr` (dev/serve mode) so production builds are unaffected. Fixes: https://github.com/vitejs/vite-plugin-vue/issues/783 Co-Authored-By: Claude Sonnet 4.6 --- .../__tests__/file-injection.spec.ts | 73 +++++++++++++++++++ packages/plugin-vue-jsx/src/index.ts | 2 + 2 files changed, 75 insertions(+) create mode 100644 packages/plugin-vue-jsx/__tests__/file-injection.spec.ts diff --git a/packages/plugin-vue-jsx/__tests__/file-injection.spec.ts b/packages/plugin-vue-jsx/__tests__/file-injection.spec.ts new file mode 100644 index 000000000..35be45eaa --- /dev/null +++ b/packages/plugin-vue-jsx/__tests__/file-injection.spec.ts @@ -0,0 +1,73 @@ +import { describe, expect, it } from 'vitest' +import type { Plugin, ResolvedConfig } from 'vite' +import vueJsxPlugin from '../src/index' + +function createPlugin() { + const plugin = vueJsxPlugin() as Plugin + // Simulate configResolved with serve + dev (needHmr = true) + const fakeConfig = { + command: 'serve', + isProduction: false, + build: { sourcemap: false }, + root: '/project', + } as unknown as ResolvedConfig + ;(plugin.configResolved as Function)(fakeConfig) + return plugin +} + +async function transform(plugin: Plugin, code: string, id: string) { + const handler = + typeof plugin.transform === 'function' + ? plugin.transform + : (plugin.transform as any)?.handler + return handler?.call({ meta: {} }, code, id, {}) +} + +const defineComponentCode = ` +import { defineComponent, ref } from 'vue' + +export const MyButton = defineComponent(() => { + const count = ref(0) + return () => +}) +` + +describe('__file injection in dev/HMR mode', () => { + it('injects __file on a named defineComponent export (.jsx)', async () => { + const plugin = createPlugin() + const id = '/project/src/components/MyButton.jsx' + const result = await transform(plugin, defineComponentCode, id) + expect(result?.code).toContain(`MyButton.__file = ${JSON.stringify(id)}`) + }) + + it('injects __file on a named defineComponent export (.tsx)', async () => { + const plugin = createPlugin() + const id = '/project/src/components/MyButton.tsx' + const tsxCode = defineComponentCode + const result = await transform(plugin, tsxCode, id) + expect(result?.code).toContain(`MyButton.__file = ${JSON.stringify(id)}`) + }) + + it('injects __file alongside __hmrId', async () => { + const plugin = createPlugin() + const id = '/project/src/MyComp.jsx' + const result = await transform(plugin, defineComponentCode, id) + expect(result?.code).toContain('__hmrId') + expect(result?.code).toContain(`__file = ${JSON.stringify(id)}`) + }) + + it('does not inject __file when not in HMR mode', async () => { + const plugin = vueJsxPlugin() as Plugin + const fakeConfig = { + command: 'build', + isProduction: true, + build: { sourcemap: false }, + root: '/project', + } as unknown as ResolvedConfig + ;(plugin.configResolved as Function)(fakeConfig) + + const id = '/project/src/MyComp.jsx' + const result = await transform(plugin, defineComponentCode, id) + expect(result?.code).not.toContain('__file') + }) +}) diff --git a/packages/plugin-vue-jsx/src/index.ts b/packages/plugin-vue-jsx/src/index.ts index fcbfe2c8e..0b177e401 100644 --- a/packages/plugin-vue-jsx/src/index.ts +++ b/packages/plugin-vue-jsx/src/index.ts @@ -327,10 +327,12 @@ function vueJsxPlugin(options: Options = {}): Plugin { if (hotComponents.length) { if (needHmr && !ssr && !/\?vue&type=script/.test(id)) { let code = result.code + const normalizedFilepath = normalizePath(filepath) let callbackCode = `` for (const { local, exported, id } of hotComponents) { code += `\n${local}.__hmrId = "${id}"` + + `\n${local}.__file = ${JSON.stringify(normalizedFilepath)}` + `\n__VUE_HMR_RUNTIME__.createRecord("${id}", ${local})` callbackCode += `\n__VUE_HMR_RUNTIME__.reload("${id}", __${exported})` } From 789ce3ca8a084cbab9e86eb09fe7c8db944ced98 Mon Sep 17 00:00:00 2001 From: Nikhil Verma Date: Mon, 18 May 2026 08:12:43 +0530 Subject: [PATCH 2/2] test(plugin-vue-jsx): add __file injection tests - Expand unit tests to cover all export shapes: named, named-via-specifier, default, and default-with-type-assertion; plus negative cases for build, SSR, and .vue script blocks. - Expose Named/Default/TsxDefault.__file via window globals in the vue-jsx playground and add corresponding e2e assertions. Co-Authored-By: Claude Sonnet 4.6 --- .../__tests__/file-injection.spec.ts | 151 ++++++++++++------ playground/vue-jsx/__tests__/vue-jsx.spec.ts | 17 ++ playground/vue-jsx/main.jsx | 5 + 3 files changed, 124 insertions(+), 49 deletions(-) diff --git a/packages/plugin-vue-jsx/__tests__/file-injection.spec.ts b/packages/plugin-vue-jsx/__tests__/file-injection.spec.ts index 35be45eaa..36000ae6e 100644 --- a/packages/plugin-vue-jsx/__tests__/file-injection.spec.ts +++ b/packages/plugin-vue-jsx/__tests__/file-injection.spec.ts @@ -2,72 +2,125 @@ import { describe, expect, it } from 'vitest' import type { Plugin, ResolvedConfig } from 'vite' import vueJsxPlugin from '../src/index' -function createPlugin() { +function createPlugin( + opts: { command?: 'serve' | 'build'; isProduction?: boolean } = {}, +) { + const { command = 'serve', isProduction = false } = opts const plugin = vueJsxPlugin() as Plugin - // Simulate configResolved with serve + dev (needHmr = true) - const fakeConfig = { - command: 'serve', - isProduction: false, + ;(plugin.configResolved as any)({ + command, + isProduction, build: { sourcemap: false }, root: '/project', - } as unknown as ResolvedConfig - ;(plugin.configResolved as Function)(fakeConfig) + } as unknown as ResolvedConfig) return plugin } -async function transform(plugin: Plugin, code: string, id: string) { - const handler = - typeof plugin.transform === 'function' - ? plugin.transform - : (plugin.transform as any)?.handler - return handler?.call({ meta: {} }, code, id, {}) +async function transform(plugin: Plugin, code: string, id: string, opts = {}) { + const t = plugin.transform as any + const handler = typeof t === 'function' ? t : t?.handler + return handler?.call({ meta: {} }, code, id, opts) } -const defineComponentCode = ` +const namedExportCode = ` import { defineComponent, ref } from 'vue' - -export const MyButton = defineComponent(() => { +export const MyComp = defineComponent(() => { const count = ref(0) - return () => + return () =>
{count.value}
}) ` -describe('__file injection in dev/HMR mode', () => { - it('injects __file on a named defineComponent export (.jsx)', async () => { - const plugin = createPlugin() - const id = '/project/src/components/MyButton.jsx' - const result = await transform(plugin, defineComponentCode, id) - expect(result?.code).toContain(`MyButton.__file = ${JSON.stringify(id)}`) - }) +const namedSpecifierCode = ` +import { defineComponent } from 'vue' +const MyComp = defineComponent(() => () =>
) +export { MyComp } +` - it('injects __file on a named defineComponent export (.tsx)', async () => { - const plugin = createPlugin() - const id = '/project/src/components/MyButton.tsx' - const tsxCode = defineComponentCode - const result = await transform(plugin, tsxCode, id) - expect(result?.code).toContain(`MyButton.__file = ${JSON.stringify(id)}`) - }) +const defaultExportCode = ` +import { defineComponent } from 'vue' +export default defineComponent(() => () =>
) +` + +const defaultExportAsCode = ` +import { defineComponent } from 'vue' +import type { DefineComponent } from 'vue' +export default defineComponent(() => () =>
) as DefineComponent +` - it('injects __file alongside __hmrId', async () => { - const plugin = createPlugin() - const id = '/project/src/MyComp.jsx' - const result = await transform(plugin, defineComponentCode, id) - expect(result?.code).toContain('__hmrId') - expect(result?.code).toContain(`__file = ${JSON.stringify(id)}`) +describe('__file injection', () => { + describe('in dev/HMR mode', () => { + it('injects __file on a named export (.jsx)', async () => { + const plugin = createPlugin() + const id = '/project/src/MyComp.jsx' + const result = await transform(plugin, namedExportCode, id) + expect(result?.code).toContain(`MyComp.__file = ${JSON.stringify(id)}`) + }) + + it('injects __file on a named export (.tsx)', async () => { + const plugin = createPlugin() + const id = '/project/src/MyComp.tsx' + const result = await transform(plugin, namedExportCode, id) + expect(result?.code).toContain(`MyComp.__file = ${JSON.stringify(id)}`) + }) + + it('injects __file on a named export via specifier', async () => { + const plugin = createPlugin() + const id = '/project/src/MyComp.jsx' + const result = await transform(plugin, namedSpecifierCode, id) + expect(result?.code).toContain(`MyComp.__file = ${JSON.stringify(id)}`) + }) + + it('injects __file on a default export', async () => { + const plugin = createPlugin() + const id = '/project/src/MyComp.jsx' + const result = await transform(plugin, defaultExportCode, id) + expect(result?.code).toContain( + `__default__.__file = ${JSON.stringify(id)}`, + ) + }) + + it('injects __file on a default export with type assertion (TSX)', async () => { + const plugin = createPlugin() + const id = '/project/src/MyComp.tsx' + const result = await transform(plugin, defaultExportAsCode, id) + expect(result?.code).toContain( + `__default__.__file = ${JSON.stringify(id)}`, + ) + }) + + it('injects __file alongside __hmrId', async () => { + const plugin = createPlugin() + const id = '/project/src/MyComp.jsx' + const result = await transform(plugin, namedExportCode, id) + expect(result?.code).toContain('__hmrId') + expect(result?.code).toContain(`__file = ${JSON.stringify(id)}`) + }) + + it('does not inject __file for .vue script blocks', async () => { + const plugin = createPlugin() + // Simulate the id format for Vue SFC script blocks + const id = '/project/src/Comp.vue?vue&type=script&lang.jsx' + const result = await transform(plugin, namedExportCode, id) + expect(result?.code).not.toContain('__file') + }) }) - it('does not inject __file when not in HMR mode', async () => { - const plugin = vueJsxPlugin() as Plugin - const fakeConfig = { - command: 'build', - isProduction: true, - build: { sourcemap: false }, - root: '/project', - } as unknown as ResolvedConfig - ;(plugin.configResolved as Function)(fakeConfig) + describe('in build/production mode', () => { + it('does not inject __file', async () => { + const plugin = createPlugin({ command: 'build', isProduction: true }) + const id = '/project/src/MyComp.jsx' + const result = await transform(plugin, namedExportCode, id) + expect(result?.code).not.toContain('__file') + }) + }) - const id = '/project/src/MyComp.jsx' - const result = await transform(plugin, defineComponentCode, id) - expect(result?.code).not.toContain('__file') + describe('in SSR mode', () => { + it('does not inject __file (uses ssrRegisterHelper instead)', async () => { + const plugin = createPlugin() + const id = '/project/src/MyComp.jsx' + const result = await transform(plugin, namedExportCode, id, { ssr: true }) + expect(result?.code).not.toContain('__file') + expect(result?.code).toContain('ssrRegisterHelper') + }) }) }) diff --git a/playground/vue-jsx/__tests__/vue-jsx.spec.ts b/playground/vue-jsx/__tests__/vue-jsx.spec.ts index 4dbc83310..04e9eeb4e 100644 --- a/playground/vue-jsx/__tests__/vue-jsx.spec.ts +++ b/playground/vue-jsx/__tests__/vue-jsx.spec.ts @@ -123,4 +123,21 @@ describe.runIf(isServe)('vue-jsx server', () => { ) await expect.poll(() => page.textContent('.setup-jsx')).toMatch('1000') }) + + describe('__file injection', () => { + test('sets __file on named jsx component', async () => { + const file = await page.evaluate(() => (window as any).__jsxFileNamed) + expect(file).toMatch(/Comps\.jsx$/) + }) + + test('sets __file on default jsx component', async () => { + const file = await page.evaluate(() => (window as any).__jsxFileDefault) + expect(file).toMatch(/Comps\.jsx$/) + }) + + test('sets __file on default tsx component', async () => { + const file = await page.evaluate(() => (window as any).__jsxFileTsx) + expect(file).toMatch(/Comp\.tsx$/) + }) + }) }) diff --git a/playground/vue-jsx/main.jsx b/playground/vue-jsx/main.jsx index 1c20bcb20..d0a6ec3bc 100644 --- a/playground/vue-jsx/main.jsx +++ b/playground/vue-jsx/main.jsx @@ -31,3 +31,8 @@ function App() { } createApp(App).mount('#app') + +// Expose __file values so e2e tests can assert they are set in dev mode +window.__jsxFileNamed = Named.__file +window.__jsxFileTsx = TsxDefault.__file +window.__jsxFileDefault = Default.__file