diff --git a/packages/nuxt/src/vite/sentryVitePlugin.ts b/packages/nuxt/src/vite/sentryVitePlugin.ts index 78c11110bf72..f301a17c9423 100644 --- a/packages/nuxt/src/vite/sentryVitePlugin.ts +++ b/packages/nuxt/src/vite/sentryVitePlugin.ts @@ -7,17 +7,16 @@ import { extractNuxtSourceMapSetting, getPluginOptions, validateDifferentSourceM /** * Creates a Vite plugin that adds the Sentry Vite plugin and validates source map settings. */ -export function createSentryViteConfigPlugin(options: { +export function validateSourceMapsOptionsPlugin(options: { nuxt: Nuxt; moduleOptions: SentryNuxtModuleOptions; sourceMapsEnabled: boolean; - shouldDeleteFilesFallback: { client: boolean; server: boolean }; }): Plugin { - const { nuxt, moduleOptions, sourceMapsEnabled, shouldDeleteFilesFallback } = options; + const { nuxt, moduleOptions, sourceMapsEnabled } = options; const isDebug = moduleOptions.debug; return { - name: 'sentry-nuxt-vite-config', + name: 'sentry-nuxt-source-map-validation', config(viteConfig: UserConfig, env: ConfigEnv) { // Only run in production builds if (!sourceMapsEnabled || env.mode === 'development' || nuxt.options?._prepare) { @@ -34,6 +33,11 @@ export function createSentryViteConfigPlugin(options: { viteConfig.build = viteConfig.build || {}; const viteSourceMap = viteConfig.build.sourcemap; + if (isDebug) { + // eslint-disable-next-line no-console + console.log(`[Sentry] Validating Vite config for the ${runtime} runtime.`); + } + // Vite source map options are the same as the Nuxt source map config options (unless overwritten) validateDifferentSourceMapSettings({ nuxtSettingKey: `sourcemap.${runtime}`, @@ -41,17 +45,6 @@ export function createSentryViteConfigPlugin(options: { otherSettingKey: 'viteConfig.build.sourcemap', otherSettingValue: viteSourceMap, }); - - if (isDebug) { - // eslint-disable-next-line no-console - console.log(`[Sentry] Adding Sentry Vite plugin to the ${runtime} runtime.`); - } - - // Add Sentry plugin by mutating the config - // Vite plugin is added on the client and server side (plugin runs for both builds) - // Nuxt client source map is 'false' by default. Warning about this will be shown already in an earlier step, and it's also documented that `nuxt.sourcemap.client` needs to be enabled. - viteConfig.plugins = viteConfig.plugins || []; - viteConfig.plugins.push(sentryVitePlugin(getPluginOptions(moduleOptions, shouldDeleteFilesFallback))); }, }; } diff --git a/packages/nuxt/src/vite/sourceMaps.ts b/packages/nuxt/src/vite/sourceMaps.ts index b270a34a50b5..c13126074871 100644 --- a/packages/nuxt/src/vite/sourceMaps.ts +++ b/packages/nuxt/src/vite/sourceMaps.ts @@ -1,10 +1,10 @@ import type { Nuxt } from '@nuxt/schema'; import { sentryRollupPlugin, type SentryRollupPluginOptions } from '@sentry/rollup-plugin'; -import type { SentryVitePluginOptions } from '@sentry/vite-plugin'; +import { sentryVitePlugin, type SentryVitePluginOptions } from '@sentry/vite-plugin'; import type { NitroConfig } from 'nitropack'; import type { Plugin } from 'vite'; import type { SentryNuxtModuleOptions } from '../common/types'; -import { createSentryViteConfigPlugin } from './sentryVitePlugin'; +import { validateSourceMapsOptionsPlugin } from './sentryVitePlugin'; /** * Whether the user enabled (true, 'hidden', 'inline') or disabled (false) source maps @@ -20,7 +20,7 @@ export type SourceMapSetting = boolean | 'hidden' | 'inline'; export function setupSourceMaps( moduleOptions: SentryNuxtModuleOptions, nuxt: Nuxt, - addVitePlugin: (plugin: Plugin | (() => Plugin), options?: { dev?: boolean; build?: boolean }) => void, + addVitePlugin: (plugin: Plugin[], options?: { dev?: boolean; build?: boolean }) => void, ): void { // TODO(v11): remove deprecated options (also from SentryNuxtModuleOptions type) @@ -81,16 +81,16 @@ export function setupSourceMaps( } }); - addVitePlugin( - createSentryViteConfigPlugin({ - nuxt, - moduleOptions, - sourceMapsEnabled, - shouldDeleteFilesFallback, - }), - // Only add source map plugin during build - { dev: false, build: true }, - ); + if (sourceMapsEnabled && !nuxt.options.dev && !nuxt.options?._prepare) { + addVitePlugin( + [ + validateSourceMapsOptionsPlugin({ nuxt, moduleOptions, sourceMapsEnabled }), + // Vite plugin is added on the client and server side (plugin runs for both builds) + ...sentryVitePlugin(getPluginOptions(moduleOptions, shouldDeleteFilesFallback)), + ], + { dev: false, build: true }, // Only add source map plugin during build + ); + } nuxt.hook('nitro:config', (nitroConfig: NitroConfig) => { if (sourceMapsEnabled && !nitroConfig.dev && !nuxt.options?._prepare) { diff --git a/packages/nuxt/test/vite/sourceMaps-nuxtHooks.test.ts b/packages/nuxt/test/vite/sourceMaps-nuxtHooks.test.ts index 4a881583ac93..24eae0a809eb 100644 --- a/packages/nuxt/test/vite/sourceMaps-nuxtHooks.test.ts +++ b/packages/nuxt/test/vite/sourceMaps-nuxtHooks.test.ts @@ -4,15 +4,16 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vites import type { SourceMapSetting } from '../../src/vite/sourceMaps'; function createMockAddVitePlugin() { - let capturedPlugin: Plugin | null = null; + let capturedPlugins: Plugin[] | null = null; - const mockAddVitePlugin = vi.fn((plugin: Plugin | (() => Plugin)) => { - capturedPlugin = typeof plugin === 'function' ? plugin() : plugin; + const mockAddVitePlugin = vi.fn((plugins: Plugin[]) => { + capturedPlugins = plugins; }); return { mockAddVitePlugin, - getCapturedPlugin: () => capturedPlugin, + getCapturedPlugin: () => capturedPlugins?.[0] ?? null, + getCapturedPlugins: () => capturedPlugins, }; } @@ -46,7 +47,7 @@ function createMockNuxt(options: { } describe('setupSourceMaps hooks', () => { - const mockSentryVitePlugin = vi.fn(() => ({ name: 'sentry-vite-plugin' })); + const mockSentryVitePlugin = vi.fn(() => [{ name: 'sentry-vite-plugin' }]); const mockSentryRollupPlugin = vi.fn(() => ({ name: 'sentry-rollup-plugin' })); const consoleLogSpy = vi.spyOn(console, 'log'); @@ -85,39 +86,27 @@ describe('setupSourceMaps hooks', () => { const plugin = getCapturedPlugin(); expect(plugin).not.toBeNull(); - expect(plugin?.name).toBe('sentry-nuxt-vite-config'); - // modules:done is called afterward. Later, the plugin is actually added + expect(plugin?.name).toBe('sentry-nuxt-source-map-validation'); }); it.each([ { label: 'prepare mode', nuxtOptions: { _prepare: true }, - viteOptions: { mode: 'production', command: 'build' as const }, - buildConfig: { build: {}, plugins: [] }, }, { label: 'dev mode', nuxtOptions: { dev: true }, - viteOptions: { mode: 'development', command: 'build' as const }, - buildConfig: { build: {}, plugins: [] }, }, - ])('does not add plugins to vite config in $label', async ({ nuxtOptions, viteOptions, buildConfig }) => { + ])('does not add plugins to vite config in $label', async ({ nuxtOptions }) => { const { setupSourceMaps } = await import('../../src/vite/sourceMaps'); const mockNuxt = createMockNuxt(nuxtOptions); - const { mockAddVitePlugin, getCapturedPlugin } = createMockAddVitePlugin(); + const { mockAddVitePlugin } = createMockAddVitePlugin(); setupSourceMaps({ debug: true }, mockNuxt as unknown as Nuxt, mockAddVitePlugin); await mockNuxt.triggerHook('modules:done'); - const plugin = getCapturedPlugin(); - expect(plugin).not.toBeNull(); - - if (plugin && typeof plugin.config === 'function') { - const viteConfig: UserConfig = buildConfig; - plugin.config(viteConfig, viteOptions); - expect(viteConfig.plugins?.length).toBe(0); - } + expect(mockAddVitePlugin).not.toHaveBeenCalled(); }); it.each([ @@ -126,19 +115,14 @@ describe('setupSourceMaps hooks', () => { ])('adds sentry vite plugin to vite config for $label in production', async ({ buildConfig }) => { const { setupSourceMaps } = await import('../../src/vite/sourceMaps'); const mockNuxt = createMockNuxt({ _prepare: false, dev: false }); - const { mockAddVitePlugin, getCapturedPlugin } = createMockAddVitePlugin(); + const { mockAddVitePlugin, getCapturedPlugins } = createMockAddVitePlugin(); setupSourceMaps({ debug: true }, mockNuxt as unknown as Nuxt, mockAddVitePlugin); - await mockNuxt.triggerHook('modules:done'); - - const plugin = getCapturedPlugin(); - expect(plugin).not.toBeNull(); - if (plugin && typeof plugin.config === 'function') { - const viteConfig: UserConfig = buildConfig; - plugin.config(viteConfig, { mode: 'production', command: 'build' }); - expect(viteConfig.plugins?.length).toBeGreaterThan(0); - } + const plugins = getCapturedPlugins(); + expect(plugins).not.toBeNull(); + expect(plugins?.length).toBeGreaterThan(0); + expect(mockSentryVitePlugin).toHaveBeenCalled(); }); }); @@ -146,15 +130,9 @@ describe('setupSourceMaps hooks', () => { it('calls sentryVitePlugin in production mode', async () => { const { setupSourceMaps } = await import('../../src/vite/sourceMaps'); const mockNuxt = createMockNuxt({ _prepare: false, dev: false }); - const { mockAddVitePlugin, getCapturedPlugin } = createMockAddVitePlugin(); + const { mockAddVitePlugin } = createMockAddVitePlugin(); setupSourceMaps({ debug: true }, mockNuxt as unknown as Nuxt, mockAddVitePlugin); - await mockNuxt.triggerHook('modules:done'); - - const plugin = getCapturedPlugin(); - if (plugin && typeof plugin.config === 'function') { - plugin.config({ build: { ssr: false }, plugins: [] }, { mode: 'production', command: 'build' }); - } expect(mockSentryVitePlugin).toHaveBeenCalled(); }); @@ -162,18 +140,12 @@ describe('setupSourceMaps hooks', () => { it.each([ { label: 'prepare mode', nuxtOptions: { _prepare: true }, viteMode: 'production' as const }, { label: 'dev mode', nuxtOptions: { dev: true }, viteMode: 'development' as const }, - ])('does not call sentryVitePlugin in $label', async ({ nuxtOptions, viteMode }) => { + ])('does not call sentryVitePlugin in $label', async ({ nuxtOptions }) => { const { setupSourceMaps } = await import('../../src/vite/sourceMaps'); const mockNuxt = createMockNuxt(nuxtOptions); - const { mockAddVitePlugin, getCapturedPlugin } = createMockAddVitePlugin(); + const { mockAddVitePlugin } = createMockAddVitePlugin(); setupSourceMaps({ debug: true }, mockNuxt as unknown as Nuxt, mockAddVitePlugin); - await mockNuxt.triggerHook('modules:done'); - - const plugin = getCapturedPlugin(); - if (plugin && typeof plugin.config === 'function') { - plugin.config({ build: {}, plugins: [] }, { mode: viteMode, command: 'build' }); - } expect(mockSentryVitePlugin).not.toHaveBeenCalled(); }); @@ -187,23 +159,16 @@ describe('setupSourceMaps hooks', () => { '.*/**/function/**/*.map', ]; - it('uses mutated shouldDeleteFilesFallback (unset → true): plugin.config() after modules:done gets fallback filesToDeleteAfterUpload', async () => { + it('sentryVitePlugin is called with fallback filesToDeleteAfterUpload when source maps are unset', async () => { const { setupSourceMaps } = await import('../../src/vite/sourceMaps'); const mockNuxt = createMockNuxt({ _prepare: false, dev: false, sourcemap: { client: undefined, server: undefined }, }); - const { mockAddVitePlugin, getCapturedPlugin } = createMockAddVitePlugin(); + const { mockAddVitePlugin } = createMockAddVitePlugin(); setupSourceMaps({ debug: false }, mockNuxt as unknown as Nuxt, mockAddVitePlugin); - await mockNuxt.triggerHook('modules:done'); - - const plugin = getCapturedPlugin(); - expect(plugin).not.toBeNull(); - if (plugin && typeof plugin.config === 'function') { - plugin.config({ build: { ssr: false }, plugins: [] }, { mode: 'production', command: 'build' }); - } expect(mockSentryVitePlugin).toHaveBeenCalledWith( expect.objectContaining({ @@ -214,28 +179,24 @@ describe('setupSourceMaps hooks', () => { ); }); - it('uses mutated shouldDeleteFilesFallback (explicitly enabled → false): plugin.config() after modules:done gets no filesToDeleteAfterUpload', async () => { + it('sentryVitePlugin is called with fallback filesToDeleteAfterUpload even when source maps are explicitly enabled', async () => { const { setupSourceMaps } = await import('../../src/vite/sourceMaps'); const mockNuxt = createMockNuxt({ _prepare: false, dev: false, sourcemap: { client: true, server: true }, }); - const { mockAddVitePlugin, getCapturedPlugin } = createMockAddVitePlugin(); + const { mockAddVitePlugin } = createMockAddVitePlugin(); setupSourceMaps({ debug: false }, mockNuxt as unknown as Nuxt, mockAddVitePlugin); - await mockNuxt.triggerHook('modules:done'); - const plugin = getCapturedPlugin(); - expect(plugin).not.toBeNull(); - if (plugin && typeof plugin.config === 'function') { - plugin.config({ build: { ssr: false }, plugins: [] }, { mode: 'production', command: 'build' }); - } - - const pluginOptions = (mockSentryVitePlugin?.mock?.calls?.[0] as unknown[])?.[0] as { - sourcemaps?: { filesToDeleteAfterUpload?: string[] }; - }; - expect(pluginOptions?.sourcemaps?.filesToDeleteAfterUpload).toBeUndefined(); + expect(mockSentryVitePlugin).toHaveBeenCalledWith( + expect.objectContaining({ + sourcemaps: expect.objectContaining({ + filesToDeleteAfterUpload: defaultFilesToDeleteAfterUpload, + }), + }), + ); }); }); @@ -286,14 +247,14 @@ describe('setupSourceMaps hooks', () => { const plugin = getCapturedPlugin(); if (plugin && typeof plugin.config === 'function') { - plugin.config({ build: { ssr: false }, plugins: [] }, { mode: 'production', command: 'build' }); + plugin.config({ build: { ssr: false }, plugins: [] } as UserConfig, { mode: 'production', command: 'build' }); } const nitroConfig = { rollupConfig: { plugins: [] as unknown[], output: {} }, dev: false }; await mockNuxt.triggerHook('nitro:config', nitroConfig); expect(consoleLogSpy).toHaveBeenCalledWith( - expect.stringContaining('[Sentry] Adding Sentry Vite plugin to the client runtime.'), + expect.stringContaining('[Sentry] Validating Vite config for the client runtime.'), ); expect(consoleLogSpy).toHaveBeenCalledWith( expect.stringContaining('[Sentry] Adding Sentry Rollup plugin to the server runtime.'), @@ -310,7 +271,7 @@ describe('setupSourceMaps hooks', () => { const plugin = getCapturedPlugin(); if (plugin && typeof plugin.config === 'function') { - plugin.config({ build: {}, plugins: [] }, { mode: 'production', command: 'build' }); + plugin.config({ build: {}, plugins: [] } as UserConfig, { mode: 'production', command: 'build' }); } await mockNuxt.triggerHook('nitro:config', { rollupConfig: { plugins: [] }, dev: false });